mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-07-10 01:05:08 +03:00
Refact/privacy mode 1 multi monitors (#15321)
* refact: privacy mdoe 1, multi-monitors Signed-off-by: fufesou <linlong1266@gmail.com> * fix: harden privacy mode overlay & capture cleanup Signed-off-by: fufesou <linlong1266@gmail.com> * Fix privacy mode edge cases after multi-monitor overlay changes Signed-off-by: fufesou <linlong1266@gmail.com> * Add missing changes Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -45,10 +45,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
|
git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow
|
||||||
|
|
||||||
# Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3
|
# Build. commit 3b79772afb754a5a1111804864616c2e81513de8, support multiple monitors
|
||||||
- name: Build the project
|
- name: Build the project
|
||||||
run: |
|
run: |
|
||||||
cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796
|
cd RustDeskTempTopMostWindow && git checkout 3b79772afb754a5a1111804864616c2e81513de8
|
||||||
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
|
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
|
||||||
|
|
||||||
- name: Archive build artifacts
|
- name: Archive build artifacts
|
||||||
|
|||||||
@@ -72,10 +72,24 @@ Widget waylandKeyboardScopeChip(BuildContext context, String text) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// macOS privacy mode blacks out all online displays, so switching the remote
|
bool _isWindowsMode1PrivacyImpl(String privacyModeImpl) {
|
||||||
// display does not weaken the local privacy protection.
|
return privacyModeImpl == kPrivacyModeImplMag ||
|
||||||
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) {
|
privacyModeImpl == kPrivacyModeImplExcludeFromCapture;
|
||||||
return pi.platform == kPeerPlatformMacOS;
|
}
|
||||||
|
|
||||||
|
// macOS privacy mode blacks out all online displays. Windows Mode 1 also
|
||||||
|
// covers every local monitor with privacy overlay windows, so remote display
|
||||||
|
// switching does not weaken local privacy protection.
|
||||||
|
//
|
||||||
|
// Keep this separate from the capture backend capability. The legacy Windows
|
||||||
|
// magnifier capturer is not reliable for multi-monitor capture; WebRTC's
|
||||||
|
// screen_capturer_win_magnifier also disables it when SM_CMONITORS != 1:
|
||||||
|
// https://webrtc.googlesource.com/src/+/1845922d5a1bf9c27deeffb4a8c8daea124434c1/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
|
||||||
|
bool allowDisplaySwitchInPrivacyMode(PeerInfo pi, String privacyModeImpl) {
|
||||||
|
return pi.platform == kPeerPlatformMacOS ||
|
||||||
|
(pi.platform == kPeerPlatformWindows &&
|
||||||
|
_isWindowsMode1PrivacyImpl(privacyModeImpl) &&
|
||||||
|
versionCmp(pi.version, '1.4.8') >= 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TTextMenu {
|
class TTextMenu {
|
||||||
@@ -964,7 +978,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
|
|
||||||
final privacyModeState = PrivacyModeState.find(id);
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
if (pi.isSupportMultiDisplay &&
|
if (pi.isSupportMultiDisplay &&
|
||||||
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
|
(privacyModeState.isEmpty ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
|
||||||
pi.displaysCount.value > 1 &&
|
pi.displaysCount.value > 1 &&
|
||||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
|
||||||
final value =
|
final value =
|
||||||
@@ -1048,7 +1063,20 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
return []; // No permission and not active, hide options.
|
return []; // No permission and not active, hide options.
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
bool checkDisplayAllowedForPrivacyMode(String targetImplKey, bool turnOn) {
|
||||||
|
if (!turnOn ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi, targetImplKey) ||
|
||||||
|
(ffiModel.pi.currentDisplay == 0 &&
|
||||||
|
!bind.sessionIsMultiUiSession(sessionId: sessionId))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info',
|
||||||
|
'Please switch to Display 1 first', '', ffi.dialogManager);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc,
|
||||||
|
String targetImplKey) {
|
||||||
final enabled = !ffiModel.viewOnly &&
|
final enabled = !ffiModel.viewOnly &&
|
||||||
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
||||||
return TToggleMenu(
|
return TToggleMenu(
|
||||||
@@ -1056,16 +1084,7 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
onChanged: enabled
|
onChanged: enabled
|
||||||
? (value) {
|
? (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
if (!allowDisplaySwitchInPrivacyMode(pi) &&
|
if (!checkDisplayAllowedForPrivacyMode(targetImplKey, value)) {
|
||||||
ffiModel.pi.currentDisplay != 0 &&
|
|
||||||
ffiModel.pi.currentDisplay != kAllDisplayValue) {
|
|
||||||
msgBox(
|
|
||||||
sessionId,
|
|
||||||
'custom-nook-nocancel-hasclose',
|
|
||||||
'info',
|
|
||||||
'Please switch to Display 1 first',
|
|
||||||
'',
|
|
||||||
ffi.dialogManager);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final option = 'privacy-mode';
|
final option = 'privacy-mode';
|
||||||
@@ -1083,7 +1102,7 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
getDefaultMenu((sid, opt) async {
|
getDefaultMenu((sid, opt) async {
|
||||||
bind.sessionToggleOption(sessionId: sid, value: opt);
|
bind.sessionToggleOption(sessionId: sid, value: opt);
|
||||||
togglePrivacyModeTime = DateTime.now();
|
togglePrivacyModeTime = DateTime.now();
|
||||||
})
|
}, kPrivacyModeImplMag)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (privacyModeImpls.isEmpty) {
|
if (privacyModeImpls.isEmpty) {
|
||||||
@@ -1097,7 +1116,7 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
bind.sessionTogglePrivacyMode(
|
bind.sessionTogglePrivacyMode(
|
||||||
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
|
sessionId: sid, implKey: implKey, on: privacyModeState.isEmpty);
|
||||||
togglePrivacyModeTime = DateTime.now();
|
togglePrivacyModeTime = DateTime.now();
|
||||||
})
|
}, implKey)
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
final visibleImpls = hasPrivacyModePermission
|
final visibleImpls = hasPrivacyModePermission
|
||||||
@@ -1118,6 +1137,9 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||||||
? (value) {
|
? (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
if (value && !hasPrivacyModePermission) return;
|
if (value && !hasPrivacyModePermission) return;
|
||||||
|
if (!checkDisplayAllowedForPrivacyMode(implKey, value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
togglePrivacyModeTime = DateTime.now();
|
togglePrivacyModeTime = DateTime.now();
|
||||||
bind.sessionTogglePrivacyMode(
|
bind.sessionTogglePrivacyMode(
|
||||||
sessionId: sessionId, implKey: implKey, on: value);
|
sessionId: sessionId, implKey: implKey, on: value);
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
|||||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||||
"supported_privacy_mode_impl";
|
"supported_privacy_mode_impl";
|
||||||
|
|
||||||
|
const String kPrivacyModeImplMag = 'privacy_mode_impl_mag';
|
||||||
|
const String kPrivacyModeImplExcludeFromCapture =
|
||||||
|
'privacy_mode_impl_exclude_from_capture';
|
||||||
|
|
||||||
const String kPeerPlatformWindows = "Windows";
|
const String kPeerPlatformWindows = "Windows";
|
||||||
const String kPeerPlatformLinux = "Linux";
|
const String kPeerPlatformLinux = "Linux";
|
||||||
const String kPeerPlatformMacOS = "Mac OS";
|
const String kPeerPlatformMacOS = "Mac OS";
|
||||||
|
|||||||
@@ -810,8 +810,9 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toolbarItems.add(Obx(() {
|
toolbarItems.add(Obx(() {
|
||||||
if ((PrivacyModeState.find(widget.id).isEmpty ||
|
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||||
allowDisplaySwitchInPrivacyMode(pi)) &&
|
if ((privacyModeState.isEmpty ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value)) &&
|
||||||
pi.displaysCount.value > 1) {
|
pi.displaysCount.value > 1) {
|
||||||
return _MonitorMenu(
|
return _MonitorMenu(
|
||||||
id: widget.id,
|
id: widget.id,
|
||||||
|
|||||||
@@ -1220,7 +1220,11 @@ void showOptions(
|
|||||||
if (image != null) {
|
if (image != null) {
|
||||||
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
|
||||||
}
|
}
|
||||||
if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
|
final privacyModeState = PrivacyModeState.find(id);
|
||||||
|
if (pi.displays.length > 1 &&
|
||||||
|
pi.currentDisplay != kAllDisplayValue &&
|
||||||
|
(privacyModeState.isEmpty ||
|
||||||
|
allowDisplaySwitchInPrivacyMode(pi, privacyModeState.value))) {
|
||||||
final cur = pi.currentDisplay;
|
final cur = pi.currentDisplay;
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
||||||
@@ -1274,8 +1278,6 @@ void showOptions(
|
|||||||
await toolbarDisplayToggle(context, id, gFFI);
|
await toolbarDisplayToggle(context, id, gFFI);
|
||||||
|
|
||||||
List<TToggleMenu> privacyModeList = [];
|
List<TToggleMenu> privacyModeList = [];
|
||||||
// privacy mode
|
|
||||||
final privacyModeState = PrivacyModeState.find(id);
|
|
||||||
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
|
||||||
privacyModeState.isNotEmpty) {
|
privacyModeState.isNotEmpty) {
|
||||||
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
|
||||||
|
|||||||
@@ -52,6 +52,33 @@ lazy_static::lazy_static! {
|
|||||||
static ref MAG_BUFFER: Mutex<(bool, Vec<u8>)> = Default::default();
|
static ref MAG_BUFFER: Mutex<(bool, Vec<u8>)> = Default::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_windows(cls: &str, name: &str) -> Result<Vec<HWND>> {
|
||||||
|
let name_c = CString::new(name)?;
|
||||||
|
let cls_c = if cls.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(CString::new(cls)?)
|
||||||
|
};
|
||||||
|
let mut hwnds = Vec::new();
|
||||||
|
unsafe {
|
||||||
|
let mut after = NULL as _;
|
||||||
|
loop {
|
||||||
|
let hwnd = FindWindowExA(
|
||||||
|
NULL as _,
|
||||||
|
after,
|
||||||
|
cls_c.as_ref().map_or(NULL as _, |c| c.as_ptr()),
|
||||||
|
name_c.as_ptr(),
|
||||||
|
);
|
||||||
|
if hwnd.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hwnds.push(hwnd);
|
||||||
|
after = hwnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(hwnds)
|
||||||
|
}
|
||||||
|
|
||||||
pub type REFWICPixelFormatGUID = *const GUID;
|
pub type REFWICPixelFormatGUID = *const GUID;
|
||||||
pub type WICPixelFormatGUID = GUID;
|
pub type WICPixelFormatGUID = GUID;
|
||||||
|
|
||||||
@@ -247,6 +274,8 @@ pub struct CapturerMag {
|
|||||||
rect: RECT,
|
rect: RECT,
|
||||||
width: usize,
|
width: usize,
|
||||||
height: usize,
|
height: usize,
|
||||||
|
excluded_window_target: Option<(String, String)>,
|
||||||
|
excluded_windows: Vec<HWND>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for CapturerMag {
|
impl Drop for CapturerMag {
|
||||||
@@ -261,6 +290,10 @@ impl CapturerMag {
|
|||||||
MagInterface::new().is_ok()
|
MagInterface::new().is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This captures through the legacy Windows Magnification API. Do not infer
|
||||||
|
// multi-monitor capture support from privacy overlay coverage: WebRTC also
|
||||||
|
// disables its magnifier capturer when SM_CMONITORS != 1.
|
||||||
|
// https://webrtc.googlesource.com/src/+/1845922d5a1bf9c27deeffb4a8c8daea124434c1/modules/desktop_capture/win/screen_capturer_win_magnifier.cc
|
||||||
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
|
pub(crate) fn new(origin: (i32, i32), width: usize, height: usize) -> Result<Self> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||||
@@ -305,6 +338,8 @@ impl CapturerMag {
|
|||||||
},
|
},
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
excluded_window_target: None,
|
||||||
|
excluded_windows: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -436,19 +471,41 @@ impl CapturerMag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
|
pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
|
||||||
let name_c = CString::new(name)?;
|
let mut hwnds = find_windows(cls, name)?;
|
||||||
|
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
|
||||||
|
self.excluded_window_target = Some((cls.to_owned(), name.to_owned()));
|
||||||
|
if hwnds.is_empty() {
|
||||||
|
self.excluded_windows.clear();
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.exclude_windows(&mut hwnds)?;
|
||||||
|
self.excluded_windows = hwnds;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn refresh_excluded_windows(&mut self) -> Result<()> {
|
||||||
|
let Some((cls, name)) = self.excluded_window_target.as_ref() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let mut hwnds = find_windows(cls, name)?;
|
||||||
|
hwnds.sort_unstable_by_key(|hwnd| *hwnd as usize);
|
||||||
|
// This runs from frame() because refreshed privacy overlays get new
|
||||||
|
// HWNDs. It is only used on the legacy magnifier backend while privacy
|
||||||
|
// mode is active; if it shows up as hot-path cost, throttle this check.
|
||||||
|
// Keep the previous filter list while privacy windows are being recreated.
|
||||||
|
if hwnds.is_empty() || hwnds == self.excluded_windows {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.exclude_windows(&mut hwnds)?;
|
||||||
|
self.excluded_windows = hwnds;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exclude_windows(&mut self, hwnds: &mut [HWND]) -> Result<bool> {
|
||||||
|
let count = hwnds.len() as _;
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut hwnd = if cls.len() == 0 {
|
|
||||||
FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr())
|
|
||||||
} else {
|
|
||||||
let cls_c = CString::new(cls).unwrap();
|
|
||||||
FindWindowExA(NULL as _, NULL as _, cls_c.as_ptr(), name_c.as_ptr())
|
|
||||||
};
|
|
||||||
|
|
||||||
if hwnd.is_null() {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(set_window_filter_list_func) =
|
if let Some(set_window_filter_list_func) =
|
||||||
self.mag_interface.set_window_filter_list_func
|
self.mag_interface.set_window_filter_list_func
|
||||||
{
|
{
|
||||||
@@ -456,16 +513,15 @@ impl CapturerMag {
|
|||||||
== set_window_filter_list_func(
|
== set_window_filter_list_func(
|
||||||
self.magnifier_window,
|
self.magnifier_window,
|
||||||
MW_FILTERMODE_EXCLUDE,
|
MW_FILTERMODE_EXCLUDE,
|
||||||
1,
|
count,
|
||||||
&mut hwnd,
|
hwnds.as_mut_ptr(),
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ErrorKind::Other,
|
ErrorKind::Other,
|
||||||
format!(
|
format!(
|
||||||
"Failed MagSetWindowFilterList for cls {} name {}, error {}",
|
"Failed MagSetWindowFilterList for {} windows, error {}",
|
||||||
cls,
|
count,
|
||||||
name,
|
|
||||||
Error::last_os_error()
|
Error::last_os_error()
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -496,6 +552,7 @@ impl CapturerMag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn frame(&mut self, data: &mut Vec<u8>) -> Result<()> {
|
pub(crate) fn frame(&mut self, data: &mut Vec<u8>) -> Result<()> {
|
||||||
|
self.refresh_excluded_windows()?;
|
||||||
Self::clear_data();
|
Self::clear_data();
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ use hbb_common::{allow_err, bail, log, ResultType};
|
|||||||
use std::{
|
use std::{
|
||||||
ffi::CString,
|
ffi::CString,
|
||||||
io::Error,
|
io::Error,
|
||||||
|
mem::size_of,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use winapi::{
|
use winapi::{
|
||||||
shared::{
|
shared::{
|
||||||
minwindef::FALSE,
|
minwindef::{BOOL, FALSE, LPARAM, TRUE},
|
||||||
ntdef::{HANDLE, NULL},
|
ntdef::{HANDLE, NULL},
|
||||||
windef::HWND,
|
windef::{HDC, HMONITOR, HWND, RECT},
|
||||||
},
|
},
|
||||||
um::{
|
um::{
|
||||||
handleapi::CloseHandle,
|
handleapi::CloseHandle,
|
||||||
@@ -31,7 +32,13 @@ pub(super) const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_mag";
|
|||||||
pub const ORIGIN_PROCESS_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe";
|
pub const ORIGIN_PROCESS_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe";
|
||||||
pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe";
|
pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe";
|
||||||
pub const INJECTED_PROCESS_EXE: &'static str = WIN_TOPMOST_INJECTED_PROCESS_EXE;
|
pub const INJECTED_PROCESS_EXE: &'static str = WIN_TOPMOST_INJECTED_PROCESS_EXE;
|
||||||
|
pub(super) const PRIVACY_WINDOW_CLASS: &'static str = "RustDeskPrivacyWindowClass";
|
||||||
pub(super) const PRIVACY_WINDOW_NAME: &'static str = "RustDeskPrivacyWindow";
|
pub(super) const PRIVACY_WINDOW_NAME: &'static str = "RustDeskPrivacyWindow";
|
||||||
|
const PRIVACY_WINDOW_WAIT_MILLIS: u128 = 1_000;
|
||||||
|
const PRIVACY_WINDOW_WAIT_EXTRA_MONITOR_MILLIS: u128 = 500;
|
||||||
|
const PRIVACY_WINDOW_POLL_INTERVAL_MILLIS: u64 = 100;
|
||||||
|
const WM_RUSTDESK_SHOW_WINDOWS: u32 = WM_APP + 3;
|
||||||
|
const WM_RUSTDESK_HIDE_WINDOWS: u32 = WM_APP + 4;
|
||||||
|
|
||||||
struct WindowHandlers {
|
struct WindowHandlers {
|
||||||
hthread: u64,
|
hthread: u64,
|
||||||
@@ -102,22 +109,17 @@ impl PrivacyMode for PrivacyModeImpl {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.handlers.is_default() {
|
let should_start_broker = self.handlers.is_default();
|
||||||
log::info!("turn_on_privacy, dll not found when started, try start");
|
if should_start_broker {
|
||||||
|
log::info!("turn_on_privacy, broker not running, try start");
|
||||||
self.start()?;
|
self.start()?;
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1_000));
|
std::thread::sleep(std::time::Duration::from_millis(1_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
let hwnd = wait_find_privacy_hwnd(0)?;
|
if let Err(e) = self.show_privacy_windows(conn_id, true) {
|
||||||
if hwnd.is_null() {
|
self.stop();
|
||||||
bail!("No privacy window created");
|
return Err(e);
|
||||||
}
|
}
|
||||||
super::win_input::hook()?;
|
|
||||||
unsafe {
|
|
||||||
ShowWindow(hwnd as _, SW_SHOW);
|
|
||||||
}
|
|
||||||
self.conn_id = conn_id;
|
|
||||||
self.hwnd = hwnd as _;
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,27 +130,33 @@ impl PrivacyMode for PrivacyModeImpl {
|
|||||||
) -> ResultType<()> {
|
) -> ResultType<()> {
|
||||||
self.check_off_conn_id(conn_id)?;
|
self.check_off_conn_id(conn_id)?;
|
||||||
super::win_input::unhook()?;
|
super::win_input::unhook()?;
|
||||||
|
let hwnds = find_privacy_hwnds()?;
|
||||||
unsafe {
|
let hide_result = set_privacy_windows_visible(&hwnds, false);
|
||||||
let hwnd = wait_find_privacy_hwnd(0)?;
|
if hide_result.is_err() {
|
||||||
if !hwnd.is_null() {
|
self.stop();
|
||||||
ShowWindow(hwnd, SW_HIDE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Continue local state cleanup even after stop(); the broker has
|
||||||
|
// been torn down, so keeping conn_id/hwnd would leave stale state.
|
||||||
if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID {
|
if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID {
|
||||||
if let Some(state) = state {
|
// Only publish the off state after the hide message was posted.
|
||||||
allow_err!(super::set_privacy_mode_state(
|
// Otherwise the peer may receive a success-like state and then a
|
||||||
conn_id,
|
// failed turn-off response for the same request.
|
||||||
state,
|
if hide_result.is_ok() {
|
||||||
PRIVACY_MODE_IMPL.to_string(),
|
if let Some(state) = state {
|
||||||
1_000
|
allow_err!(super::set_privacy_mode_state(
|
||||||
));
|
conn_id,
|
||||||
|
state,
|
||||||
|
PRIVACY_MODE_IMPL.to_string(),
|
||||||
|
1_000
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.conn_id = INVALID_PRIVACY_MODE_CONN_ID.to_owned();
|
self.conn_id = INVALID_PRIVACY_MODE_CONN_ID.to_owned();
|
||||||
|
self.hwnd = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
hide_result.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -206,8 +214,7 @@ impl PrivacyModeImpl {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hwnd = wait_find_privacy_hwnd(1_000)?;
|
if wait_find_privacy_hwnds(PRIVACY_WINDOW_WAIT_MILLIS).is_ok() {
|
||||||
if !hwnd.is_null() {
|
|
||||||
log::info!("Privacy window is ready");
|
log::info!("Privacy window is ready");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -276,14 +283,19 @@ impl PrivacyModeImpl {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
inject_dll(
|
if let Err(e) = inject_dll(
|
||||||
proc_info.hProcess,
|
proc_info.hProcess,
|
||||||
proc_info.hThread,
|
proc_info.hThread,
|
||||||
dll_file.to_string_lossy().as_ref(),
|
dll_file.to_string_lossy().as_ref(),
|
||||||
)?;
|
) {
|
||||||
|
TerminateProcess(proc_info.hProcess, 0);
|
||||||
|
CloseHandle(proc_info.hThread);
|
||||||
|
CloseHandle(proc_info.hProcess);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
if 0xffffffff == ResumeThread(proc_info.hThread) {
|
if 0xffffffff == ResumeThread(proc_info.hThread) {
|
||||||
// CloseHandle
|
TerminateProcess(proc_info.hProcess, 0);
|
||||||
CloseHandle(proc_info.hThread);
|
CloseHandle(proc_info.hThread);
|
||||||
CloseHandle(proc_info.hProcess);
|
CloseHandle(proc_info.hProcess);
|
||||||
|
|
||||||
@@ -296,9 +308,9 @@ impl PrivacyModeImpl {
|
|||||||
self.handlers.hthread = proc_info.hThread as _;
|
self.handlers.hthread = proc_info.hThread as _;
|
||||||
self.handlers.hprocess = proc_info.hProcess as _;
|
self.handlers.hprocess = proc_info.hProcess as _;
|
||||||
|
|
||||||
let hwnd = wait_find_privacy_hwnd(1_000)?;
|
if let Err(e) = wait_find_privacy_hwnds(PRIVACY_WINDOW_WAIT_MILLIS) {
|
||||||
if hwnd.is_null() {
|
self.handlers.reset();
|
||||||
bail!("Failed to get hwnd after started");
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +321,49 @@ impl PrivacyModeImpl {
|
|||||||
pub fn stop(&mut self) {
|
pub fn stop(&mut self) {
|
||||||
self.handlers.reset();
|
self.handlers.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_privacy_windows(&mut self, conn_id: i32, hook_input: bool) -> ResultType<()> {
|
||||||
|
let hwnds = wait_find_privacy_hwnds(PRIVACY_WINDOW_WAIT_MILLIS)?;
|
||||||
|
if hwnds.is_empty() {
|
||||||
|
bail!("No privacy window created");
|
||||||
|
}
|
||||||
|
|
||||||
|
if hook_input {
|
||||||
|
super::win_input::hook()?;
|
||||||
|
}
|
||||||
|
match set_privacy_windows_visible(&hwnds, true) {
|
||||||
|
Ok(_) => {
|
||||||
|
let visible_hwnds =
|
||||||
|
match wait_find_visible_privacy_hwnds(PRIVACY_WINDOW_WAIT_MILLIS) {
|
||||||
|
Ok(hwnds) => hwnds,
|
||||||
|
Err(e) => {
|
||||||
|
allow_err!(set_privacy_windows_visible(&hwnds, false));
|
||||||
|
if hook_input {
|
||||||
|
allow_err!(super::win_input::unhook());
|
||||||
|
}
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(hwnd) = visible_hwnds.first() else {
|
||||||
|
allow_err!(set_privacy_windows_visible(&hwnds, false));
|
||||||
|
if hook_input {
|
||||||
|
allow_err!(super::win_input::unhook());
|
||||||
|
}
|
||||||
|
bail!("No visible privacy window created");
|
||||||
|
};
|
||||||
|
self.conn_id = conn_id;
|
||||||
|
self.hwnd = *hwnd as _;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
allow_err!(set_privacy_windows_visible(&hwnds, false));
|
||||||
|
if hook_input {
|
||||||
|
allow_err!(super::win_input::unhook());
|
||||||
|
}
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for PrivacyModeImpl {
|
impl Drop for PrivacyModeImpl {
|
||||||
@@ -363,21 +418,217 @@ unsafe fn inject_dll<'a>(hproc: HANDLE, hthread: HANDLE, dll_file: &'a str) -> R
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn wait_find_privacy_hwnd(msecs: u128) -> ResultType<HWND> {
|
fn wait_find_privacy_hwnds(msecs: u128) -> ResultType<Vec<HWND>> {
|
||||||
|
wait_find_privacy_hwnds_impl(msecs, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_find_visible_privacy_hwnds(msecs: u128) -> ResultType<Vec<HWND>> {
|
||||||
|
wait_find_privacy_hwnds_impl(msecs, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn privacy_window_wait_millis(base_millis: u128, monitor_count: usize) -> u128 {
|
||||||
|
if base_millis == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Privacy Mode 1 creates one overlay per monitor. Keep the single-monitor
|
||||||
|
// wait as the base and add time for each extra overlay before coverage
|
||||||
|
// verification times out.
|
||||||
|
base_millis
|
||||||
|
+ (monitor_count.saturating_sub(1) as u128) * PRIVACY_WINDOW_WAIT_EXTRA_MONITOR_MILLIS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_find_privacy_hwnds_impl(msecs: u128, require_visible: bool) -> ResultType<Vec<HWND>> {
|
||||||
|
// This verifies initial turn-on coverage. If displays change during this
|
||||||
|
// short poll window, the DLL refreshes overlays asynchronously, while this
|
||||||
|
// check may still time out against the geometry sampled here.
|
||||||
|
let monitor_rects = get_monitor_rects()?;
|
||||||
|
if monitor_rects.is_empty() {
|
||||||
|
bail!("No privacy monitor found");
|
||||||
|
}
|
||||||
|
let msecs = privacy_window_wait_millis(msecs, monitor_rects.len());
|
||||||
|
|
||||||
let tm_begin = Instant::now();
|
let tm_begin = Instant::now();
|
||||||
let wndname = CString::new(PRIVACY_WINDOW_NAME)?;
|
|
||||||
loop {
|
loop {
|
||||||
unsafe {
|
let hwnds = find_privacy_hwnds()?;
|
||||||
let hwnd = FindWindowA(NULL as _, wndname.as_ptr() as _);
|
let visible_hwnds = if require_visible {
|
||||||
if !hwnd.is_null() {
|
filter_visible_hwnds(&hwnds)
|
||||||
return Ok(hwnd);
|
} else {
|
||||||
}
|
Vec::new()
|
||||||
|
};
|
||||||
|
let covered_hwnds = if require_visible {
|
||||||
|
visible_hwnds.as_slice()
|
||||||
|
} else {
|
||||||
|
hwnds.as_slice()
|
||||||
|
};
|
||||||
|
let covered = count_covered_monitors(covered_hwnds, &monitor_rects);
|
||||||
|
if covered == monitor_rects.len() {
|
||||||
|
return Ok(if require_visible {
|
||||||
|
visible_hwnds
|
||||||
|
} else {
|
||||||
|
hwnds
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if msecs == 0 || tm_begin.elapsed().as_millis() > msecs {
|
if msecs == 0 || tm_begin.elapsed().as_millis() > msecs {
|
||||||
return Ok(NULL as _);
|
let visible = if require_visible { "visible " } else { "" };
|
||||||
|
bail!(
|
||||||
|
"Expected {}privacy windows to cover {} monitors, covered {}, found {}",
|
||||||
|
visible,
|
||||||
|
monitor_rects.len(),
|
||||||
|
covered,
|
||||||
|
hwnds.len(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
std::thread::sleep(Duration::from_millis(PRIVACY_WINDOW_POLL_INTERVAL_MILLIS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_privacy_hwnds() -> ResultType<Vec<HWND>> {
|
||||||
|
let class_name = CString::new(PRIVACY_WINDOW_CLASS)?;
|
||||||
|
let wndname = CString::new(PRIVACY_WINDOW_NAME)?;
|
||||||
|
let mut hwnds = Vec::new();
|
||||||
|
unsafe {
|
||||||
|
let mut after = NULL as _;
|
||||||
|
loop {
|
||||||
|
let hwnd = FindWindowExA(
|
||||||
|
NULL as _,
|
||||||
|
after,
|
||||||
|
class_name.as_ptr() as _,
|
||||||
|
wndname.as_ptr() as _,
|
||||||
|
);
|
||||||
|
if hwnd.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hwnds.push(hwnd);
|
||||||
|
after = hwnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(hwnds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_visible_hwnds(hwnds: &[HWND]) -> Vec<HWND> {
|
||||||
|
hwnds
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|hwnd| unsafe { FALSE != IsWindowVisible(*hwnd) })
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_privacy_windows_visible(hwnds: &[HWND], show: bool) -> ResultType<usize> {
|
||||||
|
if hwnds.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
};
|
||||||
|
let message = if show {
|
||||||
|
WM_RUSTDESK_SHOW_WINDOWS
|
||||||
|
} else {
|
||||||
|
WM_RUSTDESK_HIDE_WINDOWS
|
||||||
|
};
|
||||||
|
let mut posted = 0;
|
||||||
|
let mut first_error = None;
|
||||||
|
for &hwnd in hwnds {
|
||||||
|
unsafe {
|
||||||
|
if FALSE == PostMessageA(hwnd, message, 0, 0) {
|
||||||
|
if first_error.is_none() {
|
||||||
|
first_error = Some(Error::last_os_error());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
posted += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(error) = first_error {
|
||||||
|
bail!(
|
||||||
|
"Failed to post privacy window visibility message to all privacy windows, posted {}/{}, first error {}",
|
||||||
|
posted,
|
||||||
|
hwnds.len(),
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(posted)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_monitor_rects() -> ResultType<Vec<RECT>> {
|
||||||
|
let mut rects = Vec::new();
|
||||||
|
unsafe {
|
||||||
|
if FALSE
|
||||||
|
== EnumDisplayMonitors(
|
||||||
|
NULL as _,
|
||||||
|
NULL as _,
|
||||||
|
Some(enum_monitor_rect_proc),
|
||||||
|
&mut rects as *mut Vec<RECT> as LPARAM,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"Failed EnumDisplayMonitors, error {}",
|
||||||
|
Error::last_os_error()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(rects)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "system" fn enum_monitor_rect_proc(
|
||||||
|
hmon: HMONITOR,
|
||||||
|
_hdc: HDC,
|
||||||
|
_rect: *mut RECT,
|
||||||
|
lparam: LPARAM,
|
||||||
|
) -> BOOL {
|
||||||
|
let rects = &mut *(lparam as *mut Vec<RECT>);
|
||||||
|
let mut monitor_info = MONITORINFO {
|
||||||
|
cbSize: size_of::<MONITORINFO>() as _,
|
||||||
|
rcMonitor: RECT {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
rcWork: RECT {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
dwFlags: 0,
|
||||||
|
};
|
||||||
|
if FALSE == GetMonitorInfoA(hmon, &mut monitor_info) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
rects.push(monitor_info.rcMonitor);
|
||||||
|
TRUE
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_covered_monitors(hwnds: &[HWND], monitor_rects: &[RECT]) -> usize {
|
||||||
|
let mut covered = 0;
|
||||||
|
for monitor_rect in monitor_rects {
|
||||||
|
for hwnd in hwnds {
|
||||||
|
let mut window_rect = RECT {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
if FALSE == GetWindowRect(*hwnd, &mut window_rect) {
|
||||||
|
log::warn!(
|
||||||
|
"Failed GetWindowRect for privacy window, error {}",
|
||||||
|
Error::last_os_error()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rect_covers(&window_rect, monitor_rect) {
|
||||||
|
covered += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
covered
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect_covers(window_rect: &RECT, monitor_rect: &RECT) -> bool {
|
||||||
|
window_rect.left <= monitor_rect.left
|
||||||
|
&& window_rect.top <= monitor_rect.top
|
||||||
|
&& window_rect.right >= monitor_rect.right
|
||||||
|
&& window_rect.bottom >= monitor_rect.bottom
|
||||||
|
}
|
||||||
|
|||||||
@@ -272,6 +272,10 @@ fn create_capturer(
|
|||||||
if privacy_mode_id > 0 {
|
if privacy_mode_id > 0 {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
|
// Windows Mode 1 can cover every local monitor with overlay windows,
|
||||||
|
// but the legacy magnifier capture backend is still single-monitor
|
||||||
|
// constrained. Keep display-switch gating aligned with that backend
|
||||||
|
// limit, not just the overlay coverage.
|
||||||
if let Some(c1) = crate::privacy_mode::win_mag::create_capturer(
|
if let Some(c1) = crate::privacy_mode::win_mag::create_capturer(
|
||||||
privacy_mode_id,
|
privacy_mode_id,
|
||||||
display.origin(),
|
display.origin(),
|
||||||
|
|||||||
Reference in New Issue
Block a user