Files
rustdesk/src/server/portable_service.rs
fufesou 9df486a689 fix(ipc): harden local IPC authorization and portable-service bootstrap flow (#14671)
* fix(ipc): harden ipc access

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): full cmd path, comments, simple refactor

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): portable service, ipc exit

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): Remove unused logs

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): Use SetEntriesInAclW instead of icacls

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): Comments

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): check is_reparse_point

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): shmem name, no fallback

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): Simple refactor

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): better exit and clear

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): portable service, better exit

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): comments, id -u

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: comments linux headless, rx desktop ready

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): magic number

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): update deps

Signed-off-by: fufesou <linlong1266@gmail.com>

* Update Cargo.lock

* Update Cargo.lock

* fix(ipc): harden ipc, test `identity_unavailable`

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): portable service, check dir of shmem

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): macos, better check exe allowed

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): update hbb_common

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): update hbb_common

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): harden ipc, better active uid for uinput

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): harden portable service token validation

Compare portable service IPC tokens in constant time and document the
CSPRNG source used for one-time token generation. Clarify Windows IPC
authorization comments around canonical path matching and partial peer
identity lookup.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): simple refactor

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): harden portable service token handling

Generate the portable service IPC token directly from OsRng, keep token
comparison in the IPC layer as a fixed-length byte-wise check, and document
the malformed-frame behavior for protected service IPC.

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ipc): comments

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2026-05-09 18:15:00 +08:00

1613 lines
63 KiB
Rust

use crate::{
ipc::{self, new_listener, Connection, Data, DataPortableService, IPC_TOKEN_LEN},
platform::{
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
set_path_permission_for_portable_service_shmem_file,
validate_path_for_portable_service_shmem_dir,
},
};
use core::slice;
use hbb_common::{
allow_err,
anyhow::anyhow,
bail, libc, log,
message_proto::{KeyEvent, MouseEvent},
protobuf::Message,
tokio::{self, sync::mpsc},
ResultType,
};
#[cfg(feature = "vram")]
use scrap::AdapterDevice;
use scrap::{Capturer, Frame, TraitCapturer, TraitPixelBuffer};
use shared_memory::*;
use std::{
mem::size_of,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc, Mutex,
},
time::Duration,
};
use winapi::{
shared::minwindef::{BOOL, FALSE, TRUE},
um::winuser::{self, CURSORINFO, PCURSORINFO},
};
use windows::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ};
use super::video_qos;
const SIZE_COUNTER: usize = size_of::<i32>() * 2;
const FRAME_ALIGN: usize = 64;
const ADDR_IPC_TOKEN: usize = 0;
const ADDR_CURSOR_PARA: usize = ADDR_IPC_TOKEN + IPC_TOKEN_LEN;
const ADDR_CURSOR_COUNTER: usize = ADDR_CURSOR_PARA + size_of::<CURSORINFO>();
const ADDR_CAPTURER_PARA: usize = ADDR_CURSOR_COUNTER + SIZE_COUNTER;
const ADDR_CAPTURE_FRAME_INFO: usize = ADDR_CAPTURER_PARA + size_of::<CapturerPara>();
const ADDR_CAPTURE_WOULDBLOCK: usize = ADDR_CAPTURE_FRAME_INFO + size_of::<FrameInfo>();
const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of::<i32>();
const ADDR_CAPTURE_FRAME: usize =
(ADDR_CAPTURE_FRAME_COUNTER + SIZE_COUNTER + FRAME_ALIGN - 1) / FRAME_ALIGN * FRAME_ALIGN;
const MIN_RUNTIME_SHMEM_LEN: usize = ADDR_CAPTURE_FRAME + FRAME_ALIGN;
const IPC_SUFFIX: &str = "_portable_service";
pub const SHMEM_NAME: &str = "_portable_service";
pub const SHMEM_ARG_PREFIX: &str = "--portable-service-shmem-name=";
const SHMEM_PARENT_DIR: &str = "portable_service_shmem";
const SHMEM_NAME_MAX_LEN: usize = 64;
const MAX_NACK: usize = 3;
const PORTABLE_SERVICE_STARTUP_TIMEOUT: Duration = Duration::from_secs(15);
const MAX_DXGI_FAIL_TIME: usize = 5;
#[inline]
fn is_valid_portable_service_shmem_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= SHMEM_NAME_MAX_LEN
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
}
#[inline]
pub fn portable_service_shmem_arg(name: &str) -> String {
format!("{SHMEM_ARG_PREFIX}{name}")
}
#[inline]
fn is_valid_portable_service_ipc_token(token: &str) -> bool {
token.len() == IPC_TOKEN_LEN
&& token
.bytes()
.all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
}
#[inline]
fn read_ipc_token_from_shmem(shmem: &SharedMemory) -> Option<String> {
if shmem.len() < ADDR_IPC_TOKEN + IPC_TOKEN_LEN {
log::error!(
"Portable service shared memory too small: len={}, need>={}",
shmem.len(),
ADDR_IPC_TOKEN + IPC_TOKEN_LEN
);
return None;
}
unsafe {
let ptr = shmem.as_ptr().add(ADDR_IPC_TOKEN);
let bytes = slice::from_raw_parts(ptr, IPC_TOKEN_LEN);
let end = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(IPC_TOKEN_LEN);
if end == 0 {
return None;
}
let token = std::str::from_utf8(&bytes[..end]).ok()?.to_owned();
if is_valid_portable_service_ipc_token(&token) {
Some(token)
} else {
None
}
}
}
#[inline]
fn validate_runtime_shmem_layout(shmem: &SharedMemory) -> ResultType<()> {
if shmem.len() < MIN_RUNTIME_SHMEM_LEN {
bail!(
"Portable service shared memory too small for runtime layout: len={}, need>={}",
shmem.len(),
MIN_RUNTIME_SHMEM_LEN
);
}
Ok(())
}
#[inline]
fn is_valid_capture_frame_length(shmem_len: usize, frame_len: usize) -> bool {
let frame_capacity = shmem_len.saturating_sub(ADDR_CAPTURE_FRAME);
frame_len > 0 && frame_len <= frame_capacity
}
#[inline]
fn shared_memory_flink_path_by_name(name: &str) -> ResultType<PathBuf> {
let mut dir = crate::platform::user_accessible_folder()?;
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
dir = dir.join(SHMEM_PARENT_DIR);
Ok(dir.join(format!("shared_memory{}", name)))
}
#[inline]
fn remove_shared_memory_flink_once(name: &str, log_on_error: bool, log_context: &str) -> bool {
let flink = match shared_memory_flink_path_by_name(name) {
Ok(path) => path,
Err(err) => {
if log_on_error {
log::warn!(
"{} failed to resolve portable service shared-memory flink path for '{}': {}",
log_context,
name,
err
);
}
return false;
}
};
match std::fs::remove_file(&flink) {
Ok(()) => {
log::info!(
"{} removed portable service shared-memory flink artifact: {:?}",
log_context,
flink
);
true
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => true,
Err(err) => {
if log_on_error {
log::warn!(
"{} failed to remove portable service shared-memory flink artifact {:?}: {}",
log_context,
flink,
err
);
}
false
}
}
}
#[inline]
fn write_ipc_token_to_shmem(shmem: &SharedMemory, token: &str) -> ResultType<()> {
if !is_valid_portable_service_ipc_token(token) {
bail!("Invalid portable service ipc token");
}
shmem.write(ADDR_IPC_TOKEN, token.as_bytes());
Ok(())
}
#[inline]
fn clear_ipc_token_in_shmem(shmem: &SharedMemory) {
shmem.write(ADDR_IPC_TOKEN, &[0u8; IPC_TOKEN_LEN]);
}
#[inline]
fn portable_service_arg_value_candidate_from_arg<'a>(
arg: &'a str,
prefix: &str,
) -> Option<&'a str> {
let mut value = arg.strip_prefix(prefix)?;
value = value.trim_start();
value = value
.strip_prefix('"')
.or_else(|| value.strip_prefix('\''))
.unwrap_or(value);
value = value.split_whitespace().next().unwrap_or_default();
value = value.trim_matches(|c| c == '"' || c == '\'');
Some(value)
}
#[inline]
pub fn portable_service_shmem_name_from_args() -> Option<String> {
for arg in std::env::args() {
if let Some(value) = portable_service_arg_value_candidate_from_arg(&arg, SHMEM_ARG_PREFIX) {
if is_valid_portable_service_shmem_name(value) {
return Some(value.to_owned());
}
log::error!(
"Invalid portable service shared memory name argument: '{}'",
value
);
return None;
}
}
None
}
#[inline]
pub fn has_portable_service_shmem_arg() -> bool {
std::env::args().any(|arg| arg.starts_with(SHMEM_ARG_PREFIX))
}
pub struct SharedMemory {
inner: Shmem,
}
unsafe impl Send for SharedMemory {}
unsafe impl Sync for SharedMemory {}
impl Deref for SharedMemory {
type Target = Shmem;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for SharedMemory {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl SharedMemory {
pub fn create(name: &str, size: usize) -> ResultType<Self> {
let flink = Self::flink(name.to_string())?;
let shmem = match ShmemConf::new()
.size(size)
.flink(&flink)
.force_create_flink()
.create()
{
Ok(m) => m,
Err(ShmemError::LinkExists) => {
bail!(
"Unable to force create shmem flink {}, which should not happen.",
flink
)
}
Err(e) => {
bail!("Unable to create shmem flink {} : {}", flink, e);
}
};
log::info!("Create shared memory, size: {}, flink: {}", size, flink);
if let Err(err) = set_path_permission_for_portable_service_shmem_file(Path::new(&flink)) {
// Release shmem handle first so best-effort flink cleanup has a chance to succeed.
drop(shmem);
match std::fs::remove_file(&flink) {
Ok(()) => {
log::info!(
"Create cleanup removed portable service shared-memory flink artifact: {}",
flink
);
}
Err(remove_err) if remove_err.kind() == std::io::ErrorKind::NotFound => {}
Err(remove_err) => {
log::warn!(
"Create cleanup failed to remove portable service shared-memory flink artifact {}: {}",
flink,
remove_err
);
}
}
return Err(err);
}
Ok(SharedMemory { inner: shmem })
}
pub fn open_existing(name: &str) -> ResultType<Self> {
let flink = Self::flink(name.to_string())?;
let shmem = match ShmemConf::new().flink(&flink).allow_raw(true).open() {
Ok(m) => m,
Err(e) => {
bail!("Unable to open existing shmem flink {} : {}", flink, e);
}
};
log::info!("open existing shared memory, flink: {:?}", flink);
Ok(SharedMemory { inner: shmem })
}
pub fn write(&self, addr: usize, data: &[u8]) {
unsafe {
debug_assert!(addr + data.len() <= self.inner.len());
let ptr = self.inner.as_ptr().add(addr);
let shared_mem_slice = slice::from_raw_parts_mut(ptr, data.len());
shared_mem_slice.copy_from_slice(data);
}
}
fn flink(name: String) -> ResultType<String> {
let mut dir = crate::platform::user_accessible_folder()?;
dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone());
dir = dir.join(SHMEM_PARENT_DIR);
let parent_created = !dir.exists();
if parent_created {
std::fs::create_dir_all(&dir)?;
}
if parent_created || crate::platform::is_root() {
// Harden parent ACL on first provisioning and periodically on SYSTEM path.
set_path_permission_for_portable_service_shmem_dir(&dir)?;
} else {
// Existing parents still need type/reparse validation. Non-SYSTEM callers may lack
// WRITE_DAC on a valid parent, so avoid rebuilding the ACL here.
validate_path_for_portable_service_shmem_dir(&dir)?;
}
Ok(dir
.join(format!("shared_memory{}", name))
.to_string_lossy()
.to_string())
}
}
mod utils {
use core::slice;
use std::mem::size_of;
use super::{
CapturerPara, FrameInfo, SharedMemory, ADDR_CAPTURER_PARA, ADDR_CAPTURE_FRAME_INFO,
};
#[inline]
pub fn i32_to_vec(i: i32) -> Vec<u8> {
i.to_ne_bytes().to_vec()
}
#[inline]
pub fn ptr_to_i32(ptr: *const u8) -> i32 {
unsafe {
let v = slice::from_raw_parts(ptr, size_of::<i32>());
i32::from_ne_bytes([v[0], v[1], v[2], v[3]])
}
}
#[inline]
pub fn counter_ready(counter: *const u8) -> bool {
unsafe {
let wptr = counter;
let rptr = counter.add(size_of::<i32>());
let iw = ptr_to_i32(wptr);
let ir = ptr_to_i32(rptr);
if ir != iw {
std::ptr::copy_nonoverlapping(wptr, rptr as *mut _, size_of::<i32>());
true
} else {
false
}
}
}
#[inline]
pub fn counter_equal(counter: *const u8) -> bool {
unsafe {
let wptr = counter;
let rptr = counter.add(size_of::<i32>());
let iw = ptr_to_i32(wptr);
let ir = ptr_to_i32(rptr);
iw == ir
}
}
#[inline]
pub fn increase_counter(counter: *mut u8) {
unsafe {
let wptr = counter;
let rptr = counter.add(size_of::<i32>());
let iw = ptr_to_i32(counter);
let ir = ptr_to_i32(counter);
let iw_plus1 = if iw == i32::MAX { 0 } else { iw + 1 };
let v = i32_to_vec(iw_plus1);
std::ptr::copy_nonoverlapping(v.as_ptr(), wptr, size_of::<i32>());
if ir == iw_plus1 {
let v = i32_to_vec(iw);
std::ptr::copy_nonoverlapping(v.as_ptr(), rptr, size_of::<i32>());
}
}
}
#[inline]
pub fn align(v: usize, align: usize) -> usize {
(v + align - 1) / align * align
}
#[inline]
pub fn set_para(shmem: &SharedMemory, para: CapturerPara) {
let para_ptr = &para as *const CapturerPara as *const u8;
let para_data;
unsafe {
para_data = slice::from_raw_parts(para_ptr, size_of::<CapturerPara>());
}
shmem.write(ADDR_CAPTURER_PARA, para_data);
}
#[inline]
pub fn set_frame_info(shmem: &SharedMemory, info: FrameInfo) {
let ptr = &info as *const FrameInfo as *const u8;
let data;
unsafe {
data = slice::from_raw_parts(ptr, size_of::<FrameInfo>());
}
shmem.write(ADDR_CAPTURE_FRAME_INFO, data);
}
}
// functions called in separate SYSTEM user process.
pub mod server {
use hbb_common::message_proto::PointerDeviceEvent;
use crate::display_service;
use super::*;
lazy_static::lazy_static! {
static ref EXIT: Arc<Mutex<bool>> = Default::default();
static ref FORCE_EXIT_ARMED: AtomicBool = AtomicBool::new(false);
}
pub fn run_portable_service() {
let shmem_name = match portable_service_shmem_name_from_args() {
Some(name) => name,
None => {
if has_portable_service_shmem_arg() {
log::error!(
"Invalid portable service shared memory argument, aborting startup"
);
} else {
log::error!(
"Missing portable service shared memory argument, aborting startup"
);
}
return;
}
};
let shmem = match SharedMemory::open_existing(&shmem_name) {
Ok(shmem) => Arc::new(shmem),
Err(e) => {
log::error!("Failed to open existing shared memory: {:?}", e);
return;
}
};
if let Err(e) = validate_runtime_shmem_layout(shmem.as_ref()) {
log::error!("{}", e);
return;
}
let ipc_token = match read_ipc_token_from_shmem(shmem.as_ref()) {
Some(token) => token,
None => {
log::error!(
"Missing portable service ipc token in shared memory, aborting startup"
);
return;
}
};
let shmem1 = shmem.clone();
let shmem2 = shmem.clone();
let mut threads = vec![];
threads.push(std::thread::spawn(|| {
run_get_cursor_info(shmem1);
}));
threads.push(std::thread::spawn(|| {
run_capture(shmem2);
}));
threads.push(std::thread::spawn(move || {
run_ipc_client(ipc_token);
}));
// Detached shutdown watchdog:
// - gives graceful shutdown/cleanup a short window
// - force-exits the process if workers are still stuck
std::thread::spawn(|| {
run_exit_check();
});
let record_pos_handle = crate::input_service::try_start_record_cursor_pos();
// Arm forced-exit watchdog only for worker join phase.
// Once join phase completes, cleanup should not be interrupted by forced exit.
FORCE_EXIT_ARMED.store(true, Ordering::SeqCst);
for th in threads.drain(..) {
th.join().ok();
log::info!("thread joined");
}
FORCE_EXIT_ARMED.store(false, Ordering::SeqCst);
crate::input_service::try_stop_record_cursor_pos();
if let Some(handle) = record_pos_handle {
match handle.join() {
Ok(_) => log::info!("record_pos_handle joined"),
Err(e) => log::error!("record_pos_handle join error {:?}", &e),
}
}
drop(shmem);
remove_shared_memory_flink_with_retry(&shmem_name);
}
fn run_exit_check() {
const FORCED_EXIT_DELAY: Duration = Duration::from_secs(3);
loop {
if EXIT.lock().unwrap().clone() {
break;
}
std::thread::sleep(Duration::from_millis(50));
}
// Fallback only: normal shutdown path should complete and process should exit naturally.
// This forced exit is a last resort when worker threads are stuck and graceful teardown
// does not finish in time.
std::thread::sleep(FORCED_EXIT_DELAY);
if FORCE_EXIT_ARMED.load(Ordering::SeqCst) {
log::warn!(
"Portable service shutdown watchdog fallback triggered: forcing process exit after {:?}",
FORCED_EXIT_DELAY
);
std::process::exit(0);
}
}
fn remove_shared_memory_flink_with_retry(name: &str) {
const MAX_RETRY: usize = 20;
const RETRY_INTERVAL: Duration = Duration::from_millis(200);
for attempt in 0..MAX_RETRY {
let is_last_attempt = attempt + 1 == MAX_RETRY;
if remove_shared_memory_flink_once(name, is_last_attempt, "SYSTEM cleanup") {
return;
}
if !is_last_attempt {
std::thread::sleep(RETRY_INTERVAL);
}
}
log::warn!(
"SYSTEM cleanup failed to remove portable service shared-memory flink artifact '{}' after retry",
name
);
}
fn run_get_cursor_info(shmem: Arc<SharedMemory>) {
loop {
if EXIT.lock().unwrap().clone() {
break;
}
unsafe {
let para = shmem.as_ptr().add(ADDR_CURSOR_PARA) as *mut CURSORINFO;
(*para).cbSize = size_of::<CURSORINFO>() as _;
let result = winuser::GetCursorInfo(para);
if result == TRUE {
utils::increase_counter(shmem.as_ptr().add(ADDR_CURSOR_COUNTER));
}
}
// more frequent in case of `Error of mouse_cursor service`
std::thread::sleep(Duration::from_millis(15));
}
}
fn run_capture(shmem: Arc<SharedMemory>) {
let mut c = None;
let mut last_current_display = usize::MAX;
let mut last_timeout_ms: i32 = 33;
let mut spf = Duration::from_millis(last_timeout_ms as _);
let mut first_frame_captured = false;
let mut dxgi_failed_times = 0;
let mut display_width = 0;
let mut display_height = 0;
loop {
if EXIT.lock().unwrap().clone() {
break;
}
unsafe {
let para_ptr = shmem.as_ptr().add(ADDR_CAPTURER_PARA);
let para = para_ptr as *const CapturerPara;
let recreate = (*para).recreate;
let current_display = (*para).current_display;
let timeout_ms = (*para).timeout_ms;
if c.is_none() {
let Ok(mut displays) = display_service::try_get_displays() else {
log::error!("Failed to get displays");
*EXIT.lock().unwrap() = true;
return;
};
if displays.len() <= current_display {
log::error!("Invalid display index:{}", current_display);
*EXIT.lock().unwrap() = true;
return;
}
let display = displays.remove(current_display);
display_width = display.width();
display_height = display.height();
match Capturer::new(display) {
Ok(mut v) => {
c = {
last_current_display = current_display;
first_frame_captured = false;
if dxgi_failed_times > MAX_DXGI_FAIL_TIME {
dxgi_failed_times = 0;
v.set_gdi();
}
utils::set_para(
&shmem,
CapturerPara {
recreate: false,
current_display: (*para).current_display,
timeout_ms: (*para).timeout_ms,
},
);
Some(v)
}
}
Err(e) => {
log::error!("Failed to create gdi capturer: {:?}", e);
std::thread::sleep(std::time::Duration::from_secs(1));
continue;
}
}
} else {
if recreate || current_display != last_current_display {
log::info!(
"create capturer, display: {} -> {}",
last_current_display,
current_display,
);
c = None;
continue;
}
if timeout_ms != last_timeout_ms
&& timeout_ms >= 1000 / video_qos::MAX_FPS as i32
&& timeout_ms <= 1000 / video_qos::MIN_FPS as i32
{
last_timeout_ms = timeout_ms;
spf = Duration::from_millis(timeout_ms as _);
}
}
if first_frame_captured {
if !utils::counter_equal(shmem.as_ptr().add(ADDR_CAPTURE_FRAME_COUNTER)) {
std::thread::sleep(std::time::Duration::from_millis(1));
continue;
}
}
match c.as_mut().map(|f| f.frame(spf)) {
Some(Ok(f)) => match f {
Frame::PixelBuffer(f) => {
let frame_capacity = shmem.len().saturating_sub(ADDR_CAPTURE_FRAME);
if f.data().len() > frame_capacity {
log::error!(
"Portable service capture frame exceeds shared memory capacity: frame_len={}, capacity={}, shmem_len={}",
f.data().len(),
frame_capacity,
shmem.len()
);
*EXIT.lock().unwrap() = true;
return;
}
utils::set_frame_info(
&shmem,
FrameInfo {
length: f.data().len(),
width: display_width,
height: display_height,
},
);
shmem.write(ADDR_CAPTURE_FRAME, f.data());
shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE));
utils::increase_counter(shmem.as_ptr().add(ADDR_CAPTURE_FRAME_COUNTER));
first_frame_captured = true;
dxgi_failed_times = 0;
}
Frame::Texture(_) => {
// should not happen
}
},
Some(Err(e)) => {
if crate::platform::windows::desktop_changed() {
crate::platform::try_change_desktop();
c = None;
std::thread::sleep(spf);
continue;
}
if e.kind() != std::io::ErrorKind::WouldBlock {
// DXGI_ERROR_INVALID_CALL after each success on Microsoft GPU driver
// log::error!("capture frame failed: {:?}", e);
if c.as_ref().map(|c| c.is_gdi()) == Some(false) {
// nog gdi
dxgi_failed_times += 1;
}
if dxgi_failed_times > MAX_DXGI_FAIL_TIME {
c = None;
shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(FALSE));
std::thread::sleep(spf);
}
} else {
shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE));
}
}
_ => {
println!("unreachable!");
}
}
}
}
}
#[tokio::main(flavor = "current_thread")]
async fn run_ipc_client(ipc_token: String) {
use DataPortableService::*;
let postfix = IPC_SUFFIX;
match ipc::connect(1000, postfix).await {
Ok(mut stream) => {
if let Err(err) =
ipc::portable_service_ipc_handshake_as_client(&mut stream, &ipc_token).await
{
log::error!("portable service ipc handshake failed: {}", err);
*EXIT.lock().unwrap() = true;
return;
}
let mut timer =
crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1)));
let mut nack = 0;
loop {
if *EXIT.lock().unwrap() {
log::info!("Portable service EXIT signaled, closing ipc client loop");
stream
.send(&Data::DataPortableService(WillClose))
.await
.ok();
break;
}
tokio::select! {
res = stream.next() => {
match res {
Err(err) => {
log::error!(
"ipc{} connection closed: {}",
postfix,
err
);
break;
}
Ok(Some(Data::DataPortableService(data))) => match data {
Ping => {
allow_err!(
stream
.send(&Data::DataPortableService(Pong))
.await
);
}
Pong => {
nack = 0;
}
ConnCount(Some(n)) => {
if n == 0 {
log::info!("Connection count equals 0, exit");
stream.send(&Data::DataPortableService(WillClose)).await.ok();
break;
}
}
Mouse((v, conn, username, argb, simulate, show_cursor)) => {
if let Ok(evt) = MouseEvent::parse_from_bytes(&v) {
crate::input_service::handle_mouse_(&evt, conn, username, argb, simulate, show_cursor);
}
}
Pointer((v, conn)) => {
if let Ok(evt) = PointerDeviceEvent::parse_from_bytes(&v) {
crate::input_service::handle_pointer_(&evt, conn);
}
}
Key(v) => {
if let Ok(evt) = KeyEvent::parse_from_bytes(&v) {
crate::input_service::handle_key_(&evt);
}
}
_ => {}
},
_ => {}
}
}
_ = timer.tick() => {
nack+=1;
if nack > MAX_NACK {
log::info!("max ping nack, exit");
break;
}
stream.send(&Data::DataPortableService(Ping)).await.ok();
stream.send(&Data::DataPortableService(ConnCount(None))).await.ok();
}
}
}
}
Err(e) => {
log::error!("Failed to connect portable service ipc: {:?}", e);
}
}
*EXIT.lock().unwrap() = true;
}
}
// functions called in main process.
pub mod client {
use super::*;
use crate::display_service;
use hbb_common::{anyhow::Context, message_proto::PointerDeviceEvent};
use scrap::PixelBuffer;
lazy_static::lazy_static! {
static ref RUNNING: Arc<Mutex<bool>> = Default::default();
static ref STARTING: Arc<Mutex<bool>> = Default::default();
static ref STARTING_TOKEN: AtomicU64 = AtomicU64::new(0);
static ref SHMEM: Arc<Mutex<Option<SharedMemory>>> = Default::default();
static ref SHMEM_RUNTIME_NAME: Arc<Mutex<Option<String>>> = Default::default();
static ref IPC_RUNTIME_TOKEN: Arc<Mutex<Option<String>>> = Default::default();
static ref SENDER : Mutex<mpsc::UnboundedSender<ipc::Data>> = Mutex::new(client::start_ipc_server());
static ref QUICK_SUPPORT: Arc<Mutex<bool>> = Default::default();
}
pub enum StartPara {
Direct,
Logon(String, String),
}
fn has_running_portable_service_process() -> bool {
let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase());
!crate::platform::get_pids_of_process_with_first_arg(&app_exe, "--portable-service")
.is_empty()
}
#[inline]
fn next_portable_service_shmem_name() -> String {
format!(
"{}_{}_{:08x}",
crate::portable_service::SHMEM_NAME,
std::process::id(),
hbb_common::rand::random::<u32>()
)
}
#[inline]
fn set_runtime_ipc_token(token: String) {
*IPC_RUNTIME_TOKEN.lock().unwrap() = Some(token);
}
#[inline]
fn schedule_remove_runtime_shmem_flink_retry(name: String) {
std::thread::spawn(move || {
const MAX_RETRY: usize = 20;
const RETRY_INTERVAL: Duration = Duration::from_millis(200);
for _ in 0..MAX_RETRY {
std::thread::sleep(RETRY_INTERVAL);
if remove_shared_memory_flink_once(&name, false, "Client cleanup") {
return;
}
}
log::warn!(
"Failed to remove portable service shared-memory flink artifact '{}' after retry",
name
);
});
}
#[inline]
fn clear_runtime_shmem_state() {
let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap();
let mut shmem_lock = SHMEM.lock().unwrap();
if let Some(shmem) = shmem_lock.as_mut() {
clear_ipc_token_in_shmem(shmem);
}
*shmem_lock = None;
let runtime_name = SHMEM_RUNTIME_NAME.lock().unwrap().take();
*runtime_token = None;
drop(runtime_token);
drop(shmem_lock);
if let Some(name) = runtime_name.as_deref() {
if !remove_shared_memory_flink_once(name, true, "Client cleanup") {
schedule_remove_runtime_shmem_flink_retry(name.to_owned());
}
}
}
#[inline]
fn consume_runtime_ipc_token_if_match(candidate: &str) -> (bool, Option<String>) {
let mut token = IPC_RUNTIME_TOKEN.lock().unwrap();
if !token
.as_deref()
.is_some_and(|expected| ipc::constant_time_ipc_token_eq(expected, candidate))
{
return (false, None);
}
let mut shmem_lock = SHMEM.lock().unwrap();
let matched_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone();
*token = None;
if let Some(shmem) = shmem_lock.as_mut() {
clear_ipc_token_in_shmem(shmem);
}
(true, matched_shmem_name)
}
#[inline]
fn restore_runtime_ipc_token_after_failed_handshake(
token: &str,
expected_shmem_name: Option<&str>,
) {
let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap();
if let Some(current) = runtime_token.as_deref() {
if current != token {
log::debug!(
"Skip restoring portable service ipc token after handshake failure: runtime token has changed to a newer value"
);
return;
}
}
let mut shmem_lock = SHMEM.lock().unwrap();
let current_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone();
if current_shmem_name.as_deref() != expected_shmem_name {
if runtime_token.as_deref() == Some(token) {
*runtime_token = None;
}
log::debug!(
"Skip restoring portable service ipc token after handshake failure: shared-memory instance has changed"
);
return;
}
let shmem_write_error = if let Some(shmem) = shmem_lock.as_mut() {
write_ipc_token_to_shmem(shmem, token)
.err()
.map(|err| err.to_string())
} else {
Some("shared memory unavailable".to_owned())
};
if let Some(err) = shmem_write_error {
if runtime_token.as_deref() == Some(token) {
*runtime_token = None;
}
log::warn!(
"Failed to restore portable service ipc token after handshake failure: {}",
err
);
return;
}
*runtime_token = Some(token.to_owned());
}
#[inline]
fn schedule_starting_timeout_reset(launch_token: u64) {
std::thread::spawn(move || {
std::thread::sleep(PORTABLE_SERVICE_STARTUP_TIMEOUT);
let should_reset = {
// Guard against stale watchdogs from previous launches:
// only the watchdog that matches the latest STARTING_TOKEN may reset STARTING.
let current_token = STARTING_TOKEN.load(Ordering::SeqCst);
// Keep lock guards in explicit short scopes to make it obvious
// there is no nested lock ordering (and to avoid Copilot false positives).
let starting = { *STARTING.lock().unwrap() };
let running = { *RUNNING.lock().unwrap() };
current_token == launch_token && starting && !running
};
if should_reset {
log::warn!(
"Portable service startup timeout before IPC ready, reset STARTING state"
);
*STARTING.lock().unwrap() = false;
}
});
}
// Launch flow summary:
// 1) Prepare/reset runtime shared memory + IPC token.
// 2) Start helper process (direct or logon) with shmem argument.
// 3) Keep STARTING=true until IPC ping/pong marks RUNNING, or timeout watchdog resets it.
pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> {
log::info!("start portable service");
let launch_token = {
// Keep lock guards in explicit short scopes to make it obvious
// there is no nested lock ordering (and to avoid Copilot false positives).
let running = { *RUNNING.lock().unwrap() };
let mut starting = STARTING.lock().unwrap();
if *starting && !running && !has_running_portable_service_process() {
log::warn!(
"Detected stale portable service STARTING state without running process, reset it"
);
*starting = false;
}
if *starting || running {
bail!("already running");
}
*starting = true;
STARTING_TOKEN.fetch_add(1, Ordering::SeqCst) + 1
};
let start_result = (|| -> ResultType<()> {
clear_runtime_shmem_state();
let mut shmem_lock = SHMEM.lock().unwrap();
let displays = scrap::Display::all()?;
if displays.is_empty() {
bail!("no display available!");
}
let mut max_pixel = 0;
let align = 64;
for d in displays {
let resolutions = crate::platform::resolutions(&d.name());
for r in resolutions {
let pixel =
utils::align(r.width as _, align) * utils::align(r.height as _, align);
if max_pixel < pixel {
max_pixel = pixel;
}
}
}
let shmem_size =
utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align).max(MIN_RUNTIME_SHMEM_LEN);
let shmem_name = next_portable_service_shmem_name();
if !is_valid_portable_service_shmem_name(&shmem_name) {
bail!("Generated invalid portable service shared memory name");
}
let ipc_token = ipc::generate_one_time_ipc_token()?;
// os error 112, no enough space
*shmem_lock = Some(crate::portable_service::SharedMemory::create(
&shmem_name,
shmem_size,
)?);
*SHMEM_RUNTIME_NAME.lock().unwrap() = Some(shmem_name);
shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory);
let shmem_name = SHMEM_RUNTIME_NAME
.lock()
.unwrap()
.clone()
.ok_or_else(|| anyhow!("portable service shared memory name is unavailable"))?;
let init_token_result = if let Some(shmem) = shmem_lock.as_mut() {
unsafe {
libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _);
}
write_ipc_token_to_shmem(shmem, &ipc_token)
} else {
Ok(())
};
if let Err(e) = init_token_result {
drop(shmem_lock);
clear_runtime_shmem_state();
bail!(
"Failed to initialize portable service ipc token in shared memory: {}",
e
);
};
drop(shmem_lock);
set_runtime_ipc_token(ipc_token.clone());
let portable_service_arg = format!(
"--portable-service {}",
crate::portable_service::portable_service_shmem_arg(&shmem_name)
);
{
let _sender = SENDER.lock().unwrap();
}
match para {
StartPara::Direct => {
match crate::platform::run_background(
&std::env::current_exe()?.to_string_lossy().to_string(),
&portable_service_arg,
) {
Ok(true) => {}
Ok(false) => {
clear_runtime_shmem_state();
bail!("Failed to run portable service process");
}
Err(e) => {
clear_runtime_shmem_state();
bail!("Failed to run portable service process: {}", e);
}
}
}
StartPara::Logon(username, password) => {
#[allow(unused_mut)]
let mut exe = std::env::current_exe()?.to_string_lossy().to_string();
#[cfg(feature = "flutter")]
{
if let Some(dir) = Path::new(&exe).parent() {
if let Err(err) = set_path_permission(
Path::new(dir),
FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0,
) {
clear_runtime_shmem_state();
bail!("Failed to set permission of {:?}: {}", dir, err);
}
}
}
#[cfg(not(feature = "flutter"))]
if let Some((dir, dst)) =
crate::platform::windows::portable_service_logon_helper_paths()
{
let cleanup_helper_artifacts = || {
if Path::new(&exe) != dst {
std::fs::remove_file(&dst).ok();
}
std::fs::remove_dir(&dir).ok();
};
let mut use_logon_helper_exe = false;
if let Err(err) = std::fs::create_dir_all(&dir) {
log::warn!(
"Failed to create portable service logon helper dir {:?}: {}",
dir,
err
);
} else if let Err(err) = std::fs::copy(&exe, &dst) {
log::warn!(
"Failed to copy portable service logon helper binary from '{}' to {:?}: {}",
exe,
dst,
err
);
cleanup_helper_artifacts();
} else if !dst.exists() {
log::warn!(
"Portable service logon helper binary missing after copy: {:?}",
dst
);
cleanup_helper_artifacts();
} else if let Err(err) =
set_path_permission(&dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0)
{
log::warn!(
"Failed to set portable service logon helper path permission for {:?}: {}",
dir,
err
);
cleanup_helper_artifacts();
} else {
use_logon_helper_exe = true;
}
if use_logon_helper_exe {
exe = dst.to_string_lossy().to_string();
}
}
if let Err(e) = crate::platform::windows::create_process_with_logon(
username.as_str(),
password.as_str(),
&exe,
&portable_service_arg,
) {
clear_runtime_shmem_state();
bail!("Failed to run portable service process: {}", e);
}
}
}
schedule_starting_timeout_reset(launch_token);
Ok(())
})();
if start_result.is_err() {
*STARTING.lock().unwrap() = false;
}
start_result
}
pub extern "C" fn drop_portable_service_shared_memory() {
// https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout
// Please make sure there is no print in the call stack
clear_runtime_shmem_state();
}
pub fn set_quick_support(v: bool) {
*QUICK_SUPPORT.lock().unwrap() = v;
}
pub struct CapturerPortable {
width: usize,
height: usize,
}
impl CapturerPortable {
pub fn new(current_display: usize) -> Self
where
Self: Sized,
{
let mut option = SHMEM.lock().unwrap();
if let Some(shmem) = option.as_mut() {
unsafe {
libc::memset(
shmem.as_ptr().add(ADDR_CURSOR_PARA) as _,
0,
shmem.len().saturating_sub(ADDR_CURSOR_PARA) as _,
);
}
utils::set_para(
shmem,
CapturerPara {
recreate: true,
current_display,
timeout_ms: 33,
},
);
shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE));
}
let (mut width, mut height) = (0, 0);
if let Ok(displays) = display_service::try_get_displays() {
if let Some(display) = displays.get(current_display) {
width = display.width();
height = display.height();
}
}
CapturerPortable { width, height }
}
}
impl TraitCapturer for CapturerPortable {
fn frame<'a>(&'a mut self, timeout: Duration) -> std::io::Result<Frame<'a>> {
let mut lock = SHMEM.lock().unwrap();
let shmem = lock.as_mut().ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
"shmem dropped".to_string(),
))?;
unsafe {
let base = shmem.as_ptr();
let para_ptr = base.add(ADDR_CAPTURER_PARA);
let para = para_ptr as *const CapturerPara;
if timeout.as_millis() != (*para).timeout_ms as _ {
utils::set_para(
shmem,
CapturerPara {
recreate: (*para).recreate,
current_display: (*para).current_display,
timeout_ms: timeout.as_millis() as _,
},
);
}
if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) {
let frame_info_ptr = shmem.as_ptr().add(ADDR_CAPTURE_FRAME_INFO);
let frame_info = frame_info_ptr as *const FrameInfo;
let frame_len = (*frame_info).length;
if !is_valid_capture_frame_length(shmem.len(), frame_len) {
log::error!(
"Portable service frame length exceeds shared memory capacity: frame_len={}, shmem_len={}, frame_addr={}",
frame_len,
shmem.len(),
ADDR_CAPTURE_FRAME
);
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"invalid portable service frame length".to_string(),
));
}
if (*frame_info).width != self.width || (*frame_info).height != self.height {
log::info!(
"skip frame, ({},{}) != ({},{})",
(*frame_info).width,
(*frame_info).height,
self.width,
self.height,
);
return Err(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"wouldblock error".to_string(),
));
}
let frame_ptr = base.add(ADDR_CAPTURE_FRAME);
let data = slice::from_raw_parts(frame_ptr, frame_len);
Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA(
data,
self.width,
self.height,
)))
} else {
let ptr = base.add(ADDR_CAPTURE_WOULDBLOCK);
let wouldblock = utils::ptr_to_i32(ptr);
if wouldblock == TRUE {
Err(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"wouldblock error".to_string(),
))
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"other error".to_string(),
))
}
}
}
}
// control by itself
fn is_gdi(&self) -> bool {
true
}
fn set_gdi(&mut self) -> bool {
true
}
#[cfg(feature = "vram")]
fn device(&self) -> AdapterDevice {
AdapterDevice::default()
}
#[cfg(feature = "vram")]
fn set_output_texture(&mut self, _texture: bool) {}
}
pub(super) fn start_ipc_server() -> mpsc::UnboundedSender<Data> {
let (tx, rx) = mpsc::unbounded_channel::<Data>();
std::thread::spawn(move || start_ipc_server_async(rx));
tx
}
#[tokio::main(flavor = "current_thread")]
async fn start_ipc_server_async(rx: mpsc::UnboundedReceiver<Data>) {
use DataPortableService::*;
let rx = Arc::new(tokio::sync::Mutex::new(rx));
let postfix = IPC_SUFFIX;
let quick_support = QUICK_SUPPORT.lock().unwrap().clone();
match new_listener(postfix).await {
Ok(mut incoming) => loop {
{
tokio::select! {
Some(result) = incoming.next() => {
match result {
Ok(stream) => {
let mut stream = Connection::new(stream);
if !ipc::authorize_windows_portable_service_ipc_connection(
&stream, postfix,
) {
continue;
}
let mut consumed_token: Option<String> = None;
let mut consumed_token_shmem_name: Option<String> = None;
let handshake_result =
ipc::portable_service_ipc_handshake_as_server(
&mut stream,
|token| {
let (matched, matched_shmem_name) =
consume_runtime_ipc_token_if_match(token);
if matched {
consumed_token = Some(token.to_owned());
consumed_token_shmem_name = matched_shmem_name;
true
} else {
false
}
},
)
.await;
if let Err(err) = handshake_result {
if let Some(token) = consumed_token.as_deref() {
restore_runtime_ipc_token_after_failed_handshake(
token,
consumed_token_shmem_name.as_deref(),
);
*STARTING.lock().unwrap() = false;
}
log::warn!(
"Rejected portable service ipc connection due to token handshake failure: postfix={}, err={}",
postfix,
err
);
continue;
}
log::info!("Got portable service ipc connection");
let rx_clone = rx.clone();
tokio::spawn(async move {
let mut stream = stream;
let postfix = postfix.to_owned();
let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1)));
let mut nack = 0;
let mut rx = rx_clone.lock().await;
loop {
tokio::select! {
res = stream.next() => {
match res {
Err(err) => {
log::info!(
"ipc{} connection closed: {}",
postfix,
err
);
break;
}
Ok(Some(Data::DataPortableService(data))) => match data {
Ping => {
stream.send(&Data::DataPortableService(Pong)).await.ok();
}
Pong => {
nack = 0;
*RUNNING.lock().unwrap() = true;
*STARTING.lock().unwrap() = false;
},
ConnCount(None) => {
if !quick_support {
let remote_count = crate::server::AUTHED_CONNS
.lock()
.unwrap()
.iter()
.filter(|c| c.conn_type == crate::server::AuthConnType::Remote)
.count();
stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok();
}
},
WillClose => {
log::info!("portable service will close");
break;
}
_=>{}
}
_=>{}
}
}
_ = timer.tick() => {
nack+=1;
if nack > MAX_NACK {
// In fact, this will not happen, ipc will be closed before max nack.
log::error!("max ipc nack");
break;
}
stream.send(&Data::DataPortableService(Ping)).await.ok();
}
Some(data) = rx.recv() => {
allow_err!(stream.send(&data).await);
}
}
}
*RUNNING.lock().unwrap() = false;
*STARTING.lock().unwrap() = false;
});
}
Err(err) => {
log::error!("Couldn't get portable client: {:?}", err);
}
}
}
}
}
},
Err(err) => {
log::error!("Failed to start portable service ipc server: {}", err);
}
}
}
fn ipc_send(data: Data) -> ResultType<()> {
let sender = SENDER.lock().unwrap();
sender
.send(data)
.map_err(|e| anyhow!("ipc send error:{:?}", e))
}
fn get_cursor_info_(shmem: &mut SharedMemory, pci: PCURSORINFO) -> BOOL {
unsafe {
let shmem_addr_para = shmem.as_ptr().add(ADDR_CURSOR_PARA);
if utils::counter_ready(shmem.as_ptr().add(ADDR_CURSOR_COUNTER)) {
std::ptr::copy_nonoverlapping(shmem_addr_para, pci as _, size_of::<CURSORINFO>());
return TRUE;
}
FALSE
}
}
fn handle_mouse_(
evt: &MouseEvent,
conn: i32,
username: String,
argb: u32,
simulate: bool,
show_cursor: bool,
) -> ResultType<()> {
let mut v = vec![];
evt.write_to_vec(&mut v)?;
ipc_send(Data::DataPortableService(DataPortableService::Mouse((
v,
conn,
username,
argb,
simulate,
show_cursor,
))))
}
fn handle_pointer_(evt: &PointerDeviceEvent, conn: i32) -> ResultType<()> {
let mut v = vec![];
evt.write_to_vec(&mut v)?;
ipc_send(Data::DataPortableService(DataPortableService::Pointer((
v, conn,
))))
}
fn handle_key_(evt: &KeyEvent) -> ResultType<()> {
let mut v = vec![];
evt.write_to_vec(&mut v)?;
ipc_send(Data::DataPortableService(DataPortableService::Key(v)))
}
pub fn create_capturer(
current_display: usize,
display: scrap::Display,
portable_service_running: bool,
) -> ResultType<Box<dyn TraitCapturer>> {
if portable_service_running != RUNNING.lock().unwrap().clone() {
log::info!("portable service status mismatch");
}
if portable_service_running && display.is_primary() {
log::info!("Create shared memory capturer");
return Ok(Box::new(CapturerPortable::new(current_display)));
} else {
log::debug!("Create capturer dxgi|gdi");
return Ok(Box::new(
Capturer::new(display).with_context(|| "Failed to create capturer")?,
));
}
}
pub fn get_cursor_info(pci: PCURSORINFO) -> BOOL {
if RUNNING.lock().unwrap().clone() {
let mut option = SHMEM.lock().unwrap();
option
.as_mut()
.map_or(FALSE, |sheme| get_cursor_info_(sheme, pci))
} else {
unsafe { winuser::GetCursorInfo(pci) }
}
}
pub fn handle_mouse(
evt: &MouseEvent,
conn: i32,
username: String,
argb: u32,
simulate: bool,
show_cursor: bool,
) {
if RUNNING.lock().unwrap().clone() {
crate::input_service::update_latest_input_cursor_time(conn);
handle_mouse_(evt, conn, username, argb, simulate, show_cursor).ok();
} else {
crate::input_service::handle_mouse_(evt, conn, username, argb, simulate, show_cursor);
}
}
pub fn handle_pointer(evt: &PointerDeviceEvent, conn: i32) {
if RUNNING.lock().unwrap().clone() {
crate::input_service::update_latest_input_cursor_time(conn);
handle_pointer_(evt, conn).ok();
} else {
crate::input_service::handle_pointer_(evt, conn);
}
}
pub fn handle_key(evt: &KeyEvent) {
if RUNNING.lock().unwrap().clone() {
handle_key_(evt).ok();
} else {
crate::input_service::handle_key_(evt);
}
}
pub fn running() -> bool {
RUNNING.lock().unwrap().clone()
}
}
#[repr(C)]
pub struct CapturerPara {
recreate: bool,
current_display: usize,
timeout_ms: i32,
}
#[repr(C)]
pub struct FrameInfo {
length: usize,
width: usize,
height: usize,
}
#[cfg(test)]
mod tests {
use super::{is_valid_capture_frame_length, ADDR_CAPTURE_FRAME};
#[test]
fn test_is_valid_capture_frame_length_rejects_zero_length() {
assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 1024, 0));
}
#[test]
fn test_is_valid_capture_frame_length_rejects_out_of_bounds_length() {
assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 17));
}
#[test]
fn test_is_valid_capture_frame_length_accepts_in_bounds_length() {
assert!(is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 16));
}
}