[fix] restore remote cursors on Windows without a physical mouse

This commit is contained in:
dijunkun
2026-09-16 22:20:36 +08:00
parent 1d9242515a
commit d9116200fe
13 changed files with 323 additions and 48 deletions
@@ -0,0 +1,86 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-16
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOWS_CURSOR_STATE_H_
#define _WINDOWS_CURSOR_STATE_H_
#include <remote_action.h>
#include <windows.h>
#include <chrono>
#include <vector>
#include "windows_cursor_shape.h"
namespace crossdesk {
// Used by both ordinary and secure-desktop capture. Device absence is only a
// compatibility hint: a hidden cursor with a null handle, or touch/pen
// suppression, must retain the application's normal hiding behavior.
class WindowsCursorState {
public:
void Sample(const CURSORINFO& info, bool embedded, CursorState* state) {
const bool showing = (info.flags & CURSOR_SHOWING) != 0;
state->render_mode = showing ? (embedded ? CursorRenderMode::embedded
: CursorRenderMode::separate)
: CursorRenderMode::hidden;
state->visible = state->render_mode == CursorRenderMode::separate;
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
: RemoteCursorShape::none;
state->hidden_reason = CursorHiddenReason::unspecified;
if (!showing) {
state->hidden_reason = (info.flags & CURSOR_SUPPRESSED)
? CursorHiddenReason::system_suppressed
: info.hCursor && NoPointingDevice()
? CursorHiddenReason::no_pointing_device
: CursorHiddenReason::system_hidden;
}
}
private:
// Query only for potentially recoverable hidden cursors, at most once per
// second. Require two successful absent observations. Query failures, RDP
// (whose devices may be absent from Raw Input), and present mice all disallow
// fallback and break the sequence of absent observations.
bool NoPointingDevice() {
const auto now = std::chrono::steady_clock::now();
if (now < next_device_check_) return absence_confirmed_;
next_device_check_ = now + std::chrono::seconds(1);
const bool absent = DetectDeviceAbsence();
absence_confirmed_ = absent && absent_before_;
absent_before_ = absent;
return absence_confirmed_;
}
static bool DetectDeviceAbsence() {
if (GetSystemMetrics(SM_MOUSEPRESENT) ||
GetSystemMetrics(SM_REMOTESESSION)) {
return false;
}
UINT count = 0;
if (GetRawInputDeviceList(nullptr, &count, sizeof(RAWINPUTDEVICELIST)) ==
static_cast<UINT>(-1) ||
count > 4096)
return false;
if (count == 0) return true;
std::vector<RAWINPUTDEVICELIST> devices(count);
const UINT read = GetRawInputDeviceList(devices.data(), &count,
sizeof(RAWINPUTDEVICELIST));
if (read == static_cast<UINT>(-1) || read > devices.size()) return false;
for (UINT i = 0; i < read; ++i) {
if (devices[i].dwType == RIM_TYPEMOUSE) return false;
}
return true;
}
bool absent_before_ = false;
bool absence_confirmed_ = false;
std::chrono::steady_clock::time_point next_device_check_{};
};
} // namespace crossdesk
#endif
@@ -1,6 +1,7 @@
#ifndef CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
#define CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
#include <chrono>
#include <cstdint>
#include <mutex>
@@ -14,8 +15,10 @@ struct SecureDesktopCursorSnapshot {
uint32_t shape = 0;
int32_t x = 0;
int32_t y = 0;
uint32_t render_mode = 0;
uint32_t hidden_reason = 0;
};
static_assert(sizeof(SecureDesktopCursorSnapshot) == 20);
static_assert(sizeof(SecureDesktopCursorSnapshot) == 28);
// The capture thread publishes helper samples for the GUI thread. While secure
// capture is active, even an unavailable sample must not fall back to sampling
@@ -30,13 +33,20 @@ class SecureDesktopCursorState {
void Update(const SecureDesktopCursorSnapshot& snapshot) {
std::lock_guard<std::mutex> lock(mutex_);
if (active_ && snapshot.valid) snapshot_ = snapshot;
if (active_) {
snapshot_ = snapshot;
updated_at_ = std::chrono::steady_clock::now();
}
}
bool Get(SecureDesktopCursorSnapshot* snapshot) const {
std::lock_guard<std::mutex> lock(mutex_);
if (!active_ || !snapshot) return false;
*snapshot = snapshot_;
if (std::chrono::steady_clock::now() - updated_at_ >=
std::chrono::milliseconds(1500)) {
snapshot->valid = 0;
}
return true;
}
@@ -44,6 +54,7 @@ class SecureDesktopCursorState {
mutable std::mutex mutex_;
bool active_ = false;
SecureDesktopCursorSnapshot snapshot_{};
std::chrono::steady_clock::time_point updated_at_{};
};
inline SecureDesktopCursorState& SharedSecureDesktopCursorState() {
@@ -25,7 +25,7 @@
#include "path_manager.h"
#include "rd_log.h"
#include "session_helper_shared.h"
#include "windows_cursor_shape.h"
#include "windows_cursor_state.h"
#include "windows_input_marker.h"
namespace {
@@ -1191,10 +1191,14 @@ struct SecureDesktopGdiResources {
CURSORINFO cursor{};
cursor.cbSize = sizeof(cursor);
if (GetCursorInfo(&cursor)) {
static thread_local crossdesk::WindowsCursorState cursor_state;
crossdesk::CursorState state{};
cursor_state.Sample(cursor, false, &state);
cursor_snapshot.valid = 1;
cursor_snapshot.visible = (cursor.flags & CURSOR_SHOWING) != 0;
cursor_snapshot.shape = static_cast<uint32_t>(
crossdesk::ShapeFromWindowsCursor(cursor.hCursor));
cursor_snapshot.visible = state.visible;
cursor_snapshot.shape = static_cast<uint32_t>(state.shape);
cursor_snapshot.render_mode = static_cast<uint32_t>(state.render_mode);
cursor_snapshot.hidden_reason = static_cast<uint32_t>(state.hidden_reason);
cursor_snapshot.x = cursor.ptScreenPos.x;
cursor_snapshot.y = cursor.ptScreenPos.y;
if (request.show_cursor && cursor_snapshot.visible &&
@@ -1208,6 +1212,8 @@ struct SecureDesktopGdiResources {
// A frame with an embedded cursor must not get a second cursor
// overlay from a native controller sharing the web capture path.
cursor_snapshot.visible = 0;
cursor_snapshot.render_mode =
static_cast<uint32_t>(crossdesk::CursorRenderMode::embedded);
}
}
}
@@ -35,9 +35,9 @@ inline constexpr wchar_t kCrossDeskSecureDesktopFrameMappingPrefix[] =
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
L"Global\\CrossDeskSecureDesktopFrameReady-";
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
// Version 2 adds cursor metadata to both headers. The GUI and local session
// helper must be deployed together; the remote-control wire format is unchanged.
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 2;
// Version 3 distinguishes embedded, hidden and unavailable cursor states.
// The GUI and local session helper must be deployed together.
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 3;
#pragma pack(push, 1)
struct CrossDeskSecureDesktopFrameHeader {