mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-22 20:43:22 +08:00
[fix] prevent remote input from blocking transport callbacks
This commit is contained in:
@@ -32,7 +32,7 @@ struct InteractionState {
|
|||||||
bool audio_capture_ = false;
|
bool audio_capture_ = false;
|
||||||
int screen_width_ = 1280;
|
int screen_width_ = 1280;
|
||||||
int screen_height_ = 720;
|
int screen_height_ = 720;
|
||||||
int selected_display_ = 0;
|
std::atomic<int> selected_display_{0};
|
||||||
char remote_id_display_[12] = "";
|
char remote_id_display_[12] = "";
|
||||||
unsigned char audio_buffer_[720]{};
|
unsigned char audio_buffer_[720]{};
|
||||||
int audio_len_ = 0;
|
int audio_len_ = 0;
|
||||||
@@ -77,7 +77,7 @@ struct UiState {
|
|||||||
bool show_connection_status_window_ = false;
|
bool show_connection_status_window_ = false;
|
||||||
bool fullscreen_button_pressed_ = false;
|
bool fullscreen_button_pressed_ = false;
|
||||||
bool is_client_mode_ = false;
|
bool is_client_mode_ = false;
|
||||||
bool is_server_mode_ = false;
|
std::atomic<bool> is_server_mode_{false};
|
||||||
bool reload_recent_connections_ = true;
|
bool reload_recent_connections_ = true;
|
||||||
bool show_offline_warning_window_ = false;
|
bool show_offline_warning_window_ = false;
|
||||||
std::string offline_warning_text_;
|
std::string offline_warning_text_;
|
||||||
|
|||||||
@@ -6201,6 +6201,7 @@ void GuiApplication::SyncGallery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::Cleanup() {
|
void GuiApplication::Cleanup() {
|
||||||
|
remote_input_.Stop();
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed);
|
windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed);
|
||||||
windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed);
|
windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include "platform/windows/screen_capturer/screen_capturer_win.h"
|
#include "platform/windows/screen_capturer/screen_capturer_win.h"
|
||||||
#include "platform/windows/input/mouse/mouse_controller.h"
|
|
||||||
#include "service_host.h"
|
#include "service_host.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -333,26 +332,44 @@ int SessionDeviceManager::StopScreenCapturer() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SessionDeviceManager::ReleaseRemoteMouseButtons() {
|
||||||
|
return remote_mouse_buttons_.ReleaseAll(
|
||||||
|
[this](unsigned mask) { return ReleaseMouseButtonMask(mask); });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SessionDeviceManager::ReleaseRemoteMouseButtons(const std::string& remote_id,
|
||||||
|
unsigned mask) {
|
||||||
|
return remote_mouse_buttons_.ReleaseController(
|
||||||
|
remote_id, [this](unsigned buttons) { return ReleaseMouseButtonMask(buttons); }, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned SessionDeviceManager::ReleaseMouseButtonMask(unsigned mask) {
|
||||||
|
unsigned released = mask;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mouse_controller_mutex_);
|
||||||
|
if (mouse_controller_) released &= mouse_controller_->ReleasePressedButtons(mask);
|
||||||
|
}
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
void SessionDeviceManager::ReleaseRemoteMouseButtons() {
|
|
||||||
if (auto mouse = dynamic_cast<PlatformMouseController*>(mouse_controller_))
|
|
||||||
mouse->ReleasePressedButtons();
|
|
||||||
std::lock_guard lock(service_mouse_mutex_);
|
std::lock_guard lock(service_mouse_mutex_);
|
||||||
for (int button = 0; button < 3; ++button) {
|
for (int button = 0; button < 3; ++button) {
|
||||||
const unsigned bit = 1u << button;
|
const unsigned bit = 1u << button;
|
||||||
if (!(service_mouse_buttons_ & bit)) continue;
|
if (!(service_mouse_buttons_ & mask & bit)) continue;
|
||||||
const auto response = SendCrossDeskSecureDesktopMouseInput(
|
const auto response = SendCrossDeskSecureDesktopMouseInput(
|
||||||
service_mouse_x_, service_mouse_y_, 0, (button + 1) * 2, 1000);
|
service_mouse_x_, service_mouse_y_, 0, (button + 1) * 2, 1000);
|
||||||
const auto json = nlohmann::json::parse(response, nullptr, false);
|
const auto json = nlohmann::json::parse(response, nullptr, false);
|
||||||
if (json.is_object() && json.value("ok", false)) {
|
if (json.is_object() && json.value("ok", false)) {
|
||||||
service_mouse_buttons_ &= ~bit;
|
service_mouse_buttons_ &= ~bit;
|
||||||
} else {
|
} else {
|
||||||
|
released &= ~bit;
|
||||||
LOG_WARN("Service mouse button release failed: button={}, response={}",
|
LOG_WARN("Service mouse button release failed: button={}, response={}",
|
||||||
button, response);
|
button, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
return released;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
std::string SessionDeviceManager::SendServiceMouseCommand(int x, int y,
|
std::string SessionDeviceManager::SendServiceMouseCommand(int x, int y,
|
||||||
int wheel, int flag) {
|
int wheel, int flag) {
|
||||||
std::lock_guard lock(service_mouse_mutex_);
|
std::lock_guard lock(service_mouse_mutex_);
|
||||||
@@ -398,7 +415,10 @@ bool SessionDeviceManager::SyncDisplayInfo() {
|
|||||||
});
|
});
|
||||||
if (changed) display_info_list_ = displays;
|
if (changed) display_info_list_ = displays;
|
||||||
}
|
}
|
||||||
if (changed && mouse_controller_) mouse_controller_->UpdateDisplayInfoList(displays);
|
if (changed) {
|
||||||
|
std::lock_guard lock(mouse_controller_mutex_);
|
||||||
|
if (mouse_controller_) mouse_controller_->UpdateDisplayInfoList(displays);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +454,7 @@ int SessionDeviceManager::StartMouseController() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
SyncDisplayInfo();
|
SyncDisplayInfo();
|
||||||
|
std::lock_guard lock(mouse_controller_mutex_);
|
||||||
mouse_controller_ =
|
mouse_controller_ =
|
||||||
static_cast<MouseController *>(device_controller_factory_->Create(
|
static_cast<MouseController *>(device_controller_factory_->Create(
|
||||||
DeviceControllerFactory::Device::Mouse));
|
DeviceControllerFactory::Device::Mouse));
|
||||||
@@ -452,10 +473,16 @@ int SessionDeviceManager::StartMouseController() {
|
|||||||
return init_ret;
|
return init_ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SessionDeviceManager::StopMouseController() {
|
int SessionDeviceManager::StopMouseController(bool force) {
|
||||||
#ifdef _WIN32
|
// Retain the native button state across transient cleanup failures. The UI
|
||||||
|
// can keep pumping while the input worker retries, including with no peers.
|
||||||
|
if (!force && owner_.remote_input_.HasPendingReleases()) return 1;
|
||||||
|
// A disconnect has revoked queued input. Finish its running injection and
|
||||||
|
// release callback before destroying the native controller.
|
||||||
|
owner_.remote_input_.WaitIdle();
|
||||||
|
if (!force && owner_.remote_input_.HasPendingReleases()) return 1;
|
||||||
ReleaseRemoteMouseButtons();
|
ReleaseRemoteMouseButtons();
|
||||||
#endif
|
std::lock_guard lock(mouse_controller_mutex_);
|
||||||
if (mouse_controller_) {
|
if (mouse_controller_) {
|
||||||
mouse_controller_->Destroy();
|
mouse_controller_->Destroy();
|
||||||
delete mouse_controller_;
|
delete mouse_controller_;
|
||||||
@@ -636,9 +663,9 @@ void SessionDeviceManager::UpdateInteractions() {
|
|||||||
owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ &&
|
owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ &&
|
||||||
owner_.mouse_controller_is_started_;
|
owner_.mouse_controller_is_started_;
|
||||||
if (stop_wayland_mouse_before_screen) {
|
if (stop_wayland_mouse_before_screen) {
|
||||||
LOG_INFO("Stopping Wayland mouse controller before screen capturer to "
|
if (StopMouseController() != 0) return;
|
||||||
|
LOG_INFO("Stopped Wayland mouse controller before screen capturer to "
|
||||||
"cleanly release the shared portal session");
|
"cleanly release the shared portal session");
|
||||||
StopMouseController();
|
|
||||||
owner_.mouse_controller_is_started_ = false;
|
owner_.mouse_controller_is_started_ = false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -659,8 +686,7 @@ void SessionDeviceManager::UpdateInteractions() {
|
|||||||
}
|
}
|
||||||
} else if (!owner_.start_mouse_controller_ &&
|
} else if (!owner_.start_mouse_controller_ &&
|
||||||
owner_.mouse_controller_is_started_) {
|
owner_.mouse_controller_is_started_) {
|
||||||
StopMouseController();
|
if (StopMouseController() == 0) owner_.mouse_controller_is_started_ = false;
|
||||||
owner_.mouse_controller_is_started_ = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (owner_.screen_capturer_is_started_ && mouse_controller_ && SyncDisplayInfo()) {
|
if (owner_.screen_capturer_is_started_ && mouse_controller_ && SyncDisplayInfo()) {
|
||||||
@@ -750,6 +776,7 @@ bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
|||||||
|
|
||||||
bool SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
|
bool SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
|
||||||
int selected_display) {
|
int selected_display) {
|
||||||
|
std::lock_guard lock(mouse_controller_mutex_);
|
||||||
if (mouse_controller_) {
|
if (mouse_controller_) {
|
||||||
return mouse_controller_->SendMouseCommand(action, selected_display) == 0;
|
return mouse_controller_->SendMouseCommand(action, selected_display) == 0;
|
||||||
}
|
}
|
||||||
@@ -763,19 +790,20 @@ int SessionDeviceManager::SwitchDisplay(int display_id) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionDeviceManager::ResetToInitialDisplay() {
|
InputOutcome SessionDeviceManager::ResetToInitialDisplay() {
|
||||||
if (!screen_capturer_) {
|
if (!screen_capturer_) {
|
||||||
return;
|
return InputOutcome::ignored;
|
||||||
}
|
}
|
||||||
const int ret = screen_capturer_->ResetToInitialMonitor();
|
const int ret = screen_capturer_->ResetToInitialMonitor();
|
||||||
if (ret != 0) {
|
if (ret != 0) {
|
||||||
LOG_WARN("Reset to initial display failed, ret={}", ret);
|
LOG_WARN("Reset to initial display failed, ret={}", ret);
|
||||||
return;
|
return InputOutcome::failed;
|
||||||
}
|
}
|
||||||
// Capture backends start at display 0. Reset the input mapping as well so
|
// Capture backends start at display 0. Reset the input mapping as well so
|
||||||
// the next connection cannot show that display while controlling another.
|
// the next connection cannot show that display while controlling another.
|
||||||
owner_.selected_display_ = 0;
|
owner_.selected_display_ = 0;
|
||||||
SyncDisplayInfo();
|
SyncDisplayInfo();
|
||||||
|
return InputOutcome::applied;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<DisplayInfo> SessionDeviceManager::display_info_list() const {
|
std::vector<DisplayInfo> SessionDeviceManager::display_info_list() const {
|
||||||
@@ -796,7 +824,7 @@ std::vector<HostDisplay> SessionDeviceManager::host_display_list() const {
|
|||||||
|
|
||||||
void SessionDeviceManager::DestroyDevices() {
|
void SessionDeviceManager::DestroyDevices() {
|
||||||
speaker_capture_.Shutdown();
|
speaker_capture_.Shutdown();
|
||||||
StopMouseController();
|
StopMouseController(true);
|
||||||
if (screen_capturer_) {
|
if (screen_capturer_) {
|
||||||
screen_capturer_->Destroy();
|
screen_capturer_->Destroy();
|
||||||
delete screen_capturer_;
|
delete screen_capturer_;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <remote_action.h>
|
#include <remote_action.h>
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
#include "device_controller.h"
|
#include "device_controller.h"
|
||||||
#include "display_info.h"
|
#include "display_info.h"
|
||||||
#include "features/input/captured_keyboard_queue.h"
|
#include "features/input/captured_keyboard_queue.h"
|
||||||
|
#include "features/input/remote_mouse_buttons.h"
|
||||||
#include "speaker_capture_controller.h"
|
#include "speaker_capture_controller.h"
|
||||||
|
|
||||||
struct SDL_AudioStream;
|
struct SDL_AudioStream;
|
||||||
@@ -53,8 +55,15 @@ public:
|
|||||||
int InitializeScreenCapturer();
|
int InitializeScreenCapturer();
|
||||||
int StartScreenCapturer();
|
int StartScreenCapturer();
|
||||||
int StopScreenCapturer();
|
int StopScreenCapturer();
|
||||||
|
bool ReleaseRemoteMouseButtons();
|
||||||
|
bool ReleaseRemoteMouseButtons(const std::string& remote_id,
|
||||||
|
unsigned mask = 7);
|
||||||
|
template <typename Send>
|
||||||
|
InputOutcome ApplyRemoteMouse(const std::string& remote_id, MouseFlag flag,
|
||||||
|
Send send) {
|
||||||
|
return remote_mouse_buttons_.Apply(remote_id, flag, std::move(send));
|
||||||
|
}
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
void ReleaseRemoteMouseButtons();
|
|
||||||
std::string SendServiceMouseCommand(int x, int y, int wheel, int flag);
|
std::string SendServiceMouseCommand(int x, int y, int wheel, int flag);
|
||||||
#endif
|
#endif
|
||||||
// Requests are asynchronous; Stop drains/revokes audio delivery before
|
// Requests are asynchronous; Stop drains/revokes audio delivery before
|
||||||
@@ -62,7 +71,9 @@ public:
|
|||||||
void StartSpeakerCapturer();
|
void StartSpeakerCapturer();
|
||||||
void StopSpeakerCapturer();
|
void StopSpeakerCapturer();
|
||||||
int StartMouseController();
|
int StartMouseController();
|
||||||
int StopMouseController();
|
// Returns 1 while background releases are pending. force is only used
|
||||||
|
// during final destruction after the input dispatcher has been stopped.
|
||||||
|
int StopMouseController(bool force = false);
|
||||||
int StartKeyboardCapturer();
|
int StartKeyboardCapturer();
|
||||||
int StopKeyboardCapturer();
|
int StopKeyboardCapturer();
|
||||||
|
|
||||||
@@ -79,7 +90,7 @@ public:
|
|||||||
bool extended);
|
bool extended);
|
||||||
bool SendMouseCommand(const RemoteAction &action, int selected_display);
|
bool SendMouseCommand(const RemoteAction &action, int selected_display);
|
||||||
int SwitchDisplay(int display_id);
|
int SwitchDisplay(int display_id);
|
||||||
void ResetToInitialDisplay();
|
InputOutcome ResetToInitialDisplay();
|
||||||
|
|
||||||
// Snapshot of the capture displays. The UI thread refreshes the list every
|
// Snapshot of the capture displays. The UI thread refreshes the list every
|
||||||
// tick while transport threads build host information and map mouse input
|
// tick while transport threads build host information and map mouse input
|
||||||
@@ -89,6 +100,7 @@ public:
|
|||||||
std::vector<HostDisplay> host_display_list() const;
|
std::vector<HostDisplay> host_display_list() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
unsigned ReleaseMouseButtonMask(unsigned mask);
|
||||||
void DrainCapturedKeyboardInput();
|
void DrainCapturedKeyboardInput();
|
||||||
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
|
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
|
||||||
int fps);
|
int fps);
|
||||||
@@ -107,6 +119,9 @@ private:
|
|||||||
SpeakerCaptureController speaker_capture_;
|
SpeakerCaptureController speaker_capture_;
|
||||||
DeviceControllerFactory *device_controller_factory_ = nullptr;
|
DeviceControllerFactory *device_controller_factory_ = nullptr;
|
||||||
MouseController *mouse_controller_ = nullptr;
|
MouseController *mouse_controller_ = nullptr;
|
||||||
|
RemoteMouseButtons remote_mouse_buttons_;
|
||||||
|
// Input is executed on a worker while the UI starts/stops this controller.
|
||||||
|
std::mutex mouse_controller_mutex_;
|
||||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||||
// Guards display_info_list_ (UI thread writes, transport threads read).
|
// Guards display_info_list_ (UI thread writes, transport threads read).
|
||||||
mutable std::mutex display_info_mutex_;
|
mutable std::mutex display_info_mutex_;
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
#include "remote_input_dispatcher.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
RemoteInputDispatcher::RemoteInputDispatcher(Apply apply, Release release,
|
||||||
|
Report report,
|
||||||
|
ResetDisplay reset_display,
|
||||||
|
ReleaseMouse release_mouse)
|
||||||
|
: apply_(std::move(apply)),
|
||||||
|
release_(std::move(release)),
|
||||||
|
report_(std::move(report)),
|
||||||
|
reset_display_(std::move(reset_display)),
|
||||||
|
release_mouse_(std::move(release_mouse)),
|
||||||
|
worker_([this] { Run(); }) {}
|
||||||
|
|
||||||
|
RemoteInputDispatcher::~RemoteInputDispatcher() { Stop(); }
|
||||||
|
|
||||||
|
bool RemoteInputDispatcher::Accepts(const RemoteAction& action) {
|
||||||
|
return action.type == ControlType::mouse ||
|
||||||
|
action.type == ControlType::keyboard ||
|
||||||
|
action.type == ControlType::keyboard_state ||
|
||||||
|
action.type == ControlType::text_input ||
|
||||||
|
action.type == ControlType::display_id ||
|
||||||
|
action.type == ControlType::service_command;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::Connect(const std::string& id) {
|
||||||
|
if (id.empty()) return;
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
if (stopping_ || !connected_.insert(id).second) return;
|
||||||
|
logs_.erase(id); // A new session must get its first diagnostic.
|
||||||
|
// In-flight resets finish before this session's input; queued old resets
|
||||||
|
// must not change the new session's display. Failed releases are retained.
|
||||||
|
RemoveResetsLocked();
|
||||||
|
ready_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteInputDispatcher::Result RemoteInputDispatcher::Push(
|
||||||
|
const std::string& id, const RemoteAction& action) {
|
||||||
|
// HostInfo owns raw arrays; it must never enter this value-copy queue.
|
||||||
|
if (!Accepts(action)) return Result::rejected;
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
if (stopping_ || !connected_.count(id)) return Result::rejected;
|
||||||
|
if (IsMove(action) && !pending_.empty()) {
|
||||||
|
auto& last = pending_.back();
|
||||||
|
if (last.id == id && last.IsInput() && IsMove(*last.action)) {
|
||||||
|
last.action = action;
|
||||||
|
last.queued_at = Clock::now();
|
||||||
|
return Result::coalesced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pending_counts_[id] >= kMaxPendingPerController) {
|
||||||
|
QueueReleaseLocked(id);
|
||||||
|
return Result::overflow;
|
||||||
|
}
|
||||||
|
pending_.push_back({id, action, Clock::now(), ItemKind::input});
|
||||||
|
++pending_counts_[id];
|
||||||
|
ready_.notify_one();
|
||||||
|
return Result::queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::Disconnect(const std::string& id) {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
if (stopping_ || !connected_.erase(id)) return;
|
||||||
|
QueueReleaseLocked(id);
|
||||||
|
if (connected_.empty() && reset_display_) {
|
||||||
|
RemoveResetsLocked();
|
||||||
|
pending_.push_back(
|
||||||
|
{{}, std::nullopt, Clock::now(), ItemKind::reset_display});
|
||||||
|
}
|
||||||
|
ready_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::WaitIdle() {
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
idle_.wait(lock, [this] { return pending_.empty() && !executing_; });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RemoteInputDispatcher::HasPendingReleases() {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
return !blocked_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::Stop() {
|
||||||
|
std::call_once(stop_once_, [this] {
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
stopping_ = true;
|
||||||
|
auto ids = std::move(connected_);
|
||||||
|
connected_.clear();
|
||||||
|
ids.insert(blocked_.begin(), blocked_.end());
|
||||||
|
for (const auto& item : pending_)
|
||||||
|
if (!item.IsReset()) ids.insert(item.id);
|
||||||
|
pending_.clear();
|
||||||
|
pending_counts_.clear();
|
||||||
|
retries_.clear();
|
||||||
|
for (const auto& id : ids) {
|
||||||
|
pending_.push_back({id, std::nullopt, Clock::now()});
|
||||||
|
pending_counts_[id] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ready_.notify_one();
|
||||||
|
worker_.join();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RemoteInputDispatcher::IsMove(const RemoteAction& action) {
|
||||||
|
return action.type == ControlType::mouse && action.m.flag == MouseFlag::move;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned RemoteInputDispatcher::MouseUpMask(const RemoteAction& action) {
|
||||||
|
if (action.type != ControlType::mouse) return 0;
|
||||||
|
switch (action.m.flag) {
|
||||||
|
case MouseFlag::left_up:
|
||||||
|
return 1;
|
||||||
|
case MouseFlag::right_up:
|
||||||
|
return 2;
|
||||||
|
case MouseFlag::middle_up:
|
||||||
|
return 4;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::RemoveResetsLocked() {
|
||||||
|
pending_.erase(
|
||||||
|
std::remove_if(pending_.begin(), pending_.end(),
|
||||||
|
[](const Item& item) { return item.IsReset(); }),
|
||||||
|
pending_.end());
|
||||||
|
if (pending_.empty() && !executing_) idle_.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::QueueReleaseLocked(const std::string& id) {
|
||||||
|
pending_.erase(
|
||||||
|
std::remove_if(pending_.begin(), pending_.end(),
|
||||||
|
[&](const Item& item) { return item.id == id; }),
|
||||||
|
pending_.end());
|
||||||
|
pending_counts_.erase(id);
|
||||||
|
blocked_.insert(id);
|
||||||
|
// Repeated overflow/disconnect must not defeat the retry backoff.
|
||||||
|
if (auto retry = retries_.find(id); retry != retries_.end()) {
|
||||||
|
retry->second.mouse_mask = 0; // Escalate a button repair on disconnect.
|
||||||
|
} else {
|
||||||
|
pending_.push_back({id, std::nullopt, Clock::now()});
|
||||||
|
pending_counts_[id] = 1;
|
||||||
|
}
|
||||||
|
ready_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::QueueMouseReleaseLocked(const std::string& id,
|
||||||
|
unsigned mask) {
|
||||||
|
// A concurrent disconnect/overflow already requested the broader cleanup.
|
||||||
|
if (!blocked_.insert(id).second) return;
|
||||||
|
// Keep subsequent clicks/keys queued, but don't apply them until the old
|
||||||
|
// button is confirmed up. Never replay a timed-out down/click operation.
|
||||||
|
pending_.push_front(
|
||||||
|
{id, std::nullopt, Clock::now(), ItemKind::cleanup, mask});
|
||||||
|
++pending_counts_[id];
|
||||||
|
ready_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<RemoteInputDispatcher::Item> RemoteInputDispatcher::NextLocked() {
|
||||||
|
const auto now = Clock::now();
|
||||||
|
auto next = retries_.end();
|
||||||
|
for (auto it = retries_.begin(); it != retries_.end(); ++it)
|
||||||
|
if (next == retries_.end() || it->second.due < next->second.due) next = it;
|
||||||
|
auto it =
|
||||||
|
std::find_if(pending_.begin(), pending_.end(), [&](const Item& item) {
|
||||||
|
return !item.IsInput() || !blocked_.count(item.id);
|
||||||
|
});
|
||||||
|
// Alternate due retries with runnable queued work. Multiple slow cleanup
|
||||||
|
// calls must not monopolize this worker; continuous input must not starve
|
||||||
|
// recovery either. Input for the recovering controller remains blocked.
|
||||||
|
if (next != retries_.end() && next->second.due <= now &&
|
||||||
|
(it == pending_.end() || !last_was_retry_)) {
|
||||||
|
last_was_retry_ = true;
|
||||||
|
return Item{next->first, std::nullopt, next->second.due, ItemKind::cleanup,
|
||||||
|
next->second.mouse_mask};
|
||||||
|
}
|
||||||
|
if (it == pending_.end()) return std::nullopt;
|
||||||
|
last_was_retry_ = false;
|
||||||
|
Item item = std::move(*it);
|
||||||
|
pending_.erase(it);
|
||||||
|
if (!item.IsReset() && --pending_counts_.at(item.id) == 0)
|
||||||
|
pending_counts_.erase(item.id);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RemoteInputDispatcher::ShouldReportLocked(const Item& item,
|
||||||
|
Clock::time_point now,
|
||||||
|
int64_t queued_ms,
|
||||||
|
int64_t execution_ms,
|
||||||
|
InputOutcome outcome) {
|
||||||
|
if (!report_) return false;
|
||||||
|
auto& state = logs_[item.id];
|
||||||
|
auto& last = outcome == InputOutcome::failed
|
||||||
|
? state.failure[item.IsInput() ? 0 : 1]
|
||||||
|
: state.slow;
|
||||||
|
if (outcome != InputOutcome::failed && queued_ms < 50 && execution_ms < 50)
|
||||||
|
return false;
|
||||||
|
if (last != Clock::time_point{} && now - last < std::chrono::seconds(2))
|
||||||
|
return false;
|
||||||
|
last = now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::RemoveQueuedCleanupLocked(const std::string& id) {
|
||||||
|
// Cleanup queued during the running call covers the same old state. Merge
|
||||||
|
// duplicates so they cannot bypass backoff or release a later session twice.
|
||||||
|
const size_t before = pending_.size();
|
||||||
|
pending_.erase(std::remove_if(pending_.begin(), pending_.end(),
|
||||||
|
[&](const Item& queued) {
|
||||||
|
return queued.id == id && queued.IsCleanup();
|
||||||
|
}),
|
||||||
|
pending_.end());
|
||||||
|
const size_t removed = before - pending_.size();
|
||||||
|
if (removed && (pending_counts_.at(id) -= removed) == 0)
|
||||||
|
pending_counts_.erase(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::ScheduleRetryLocked(const std::string& id,
|
||||||
|
unsigned mouse_mask,
|
||||||
|
Clock::time_point finished) {
|
||||||
|
auto [retry, inserted] = retries_.try_emplace(id);
|
||||||
|
retry->second.mouse_mask = mouse_mask;
|
||||||
|
if (!inserted)
|
||||||
|
retry->second.delay =
|
||||||
|
std::min(retry->second.delay * 2, std::chrono::milliseconds(1000));
|
||||||
|
retry->second.due = finished + retry->second.delay;
|
||||||
|
blocked_.insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::CompleteCleanupLocked(const Item& item,
|
||||||
|
InputOutcome outcome,
|
||||||
|
Clock::time_point finished) {
|
||||||
|
if (stopping_) {
|
||||||
|
retries_.erase(item.id);
|
||||||
|
blocked_.erase(item.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A successful button repair cannot discharge a concurrent request to
|
||||||
|
// release all keyboard/mouse state after disconnect or queue overflow.
|
||||||
|
const bool queued_full_cleanup =
|
||||||
|
item.mouse_mask &&
|
||||||
|
std::any_of(pending_.begin(), pending_.end(), [&](const Item& queued) {
|
||||||
|
return queued.id == item.id && queued.IsCleanup() && !queued.mouse_mask;
|
||||||
|
});
|
||||||
|
if (queued_full_cleanup) {
|
||||||
|
retries_.erase(item.id); // The queued full cleanup owns recovery now.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto retry = retries_.find(item.id);
|
||||||
|
if (item.mouse_mask && retry != retries_.end() && !retry->second.mouse_mask) {
|
||||||
|
if (outcome == InputOutcome::failed)
|
||||||
|
ScheduleRetryLocked(item.id, 0, finished);
|
||||||
|
else
|
||||||
|
retry->second.due = finished;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RemoveQueuedCleanupLocked(item.id);
|
||||||
|
if (outcome == InputOutcome::failed) {
|
||||||
|
ScheduleRetryLocked(item.id, item.mouse_mask, finished);
|
||||||
|
} else {
|
||||||
|
retries_.erase(item.id);
|
||||||
|
blocked_.erase(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputOutcome RemoteInputDispatcher::Execute(const Item& item) {
|
||||||
|
if (item.IsReset()) return reset_display_();
|
||||||
|
if (item.IsInput()) return apply_(item.id, *item.action);
|
||||||
|
if (item.mouse_mask && release_mouse_)
|
||||||
|
return release_mouse_(item.id, item.mouse_mask);
|
||||||
|
return release_(item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoteInputDispatcher::Run() {
|
||||||
|
std::unique_lock lock(mutex_);
|
||||||
|
for (;;) {
|
||||||
|
auto next = NextLocked();
|
||||||
|
if (!next) {
|
||||||
|
if (stopping_) return;
|
||||||
|
auto due = Clock::time_point::max();
|
||||||
|
for (const auto& [_, retry] : retries_) due = std::min(due, retry.due);
|
||||||
|
if (due == Clock::time_point::max())
|
||||||
|
ready_.wait(lock);
|
||||||
|
else
|
||||||
|
ready_.wait_until(lock, due);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Item item = std::move(*next);
|
||||||
|
executing_ = true;
|
||||||
|
lock.unlock();
|
||||||
|
const auto started = Clock::now();
|
||||||
|
InputOutcome outcome = InputOutcome::failed;
|
||||||
|
bool threw = false;
|
||||||
|
try {
|
||||||
|
outcome = Execute(item);
|
||||||
|
} catch (...) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
const auto finished = Clock::now();
|
||||||
|
const auto queued_ms =
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(started -
|
||||||
|
item.queued_at)
|
||||||
|
.count();
|
||||||
|
const auto execution_ms =
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(finished -
|
||||||
|
started)
|
||||||
|
.count();
|
||||||
|
lock.lock();
|
||||||
|
if (item.IsCleanup()) {
|
||||||
|
CompleteCleanupLocked(item, outcome, finished);
|
||||||
|
} else if (item.IsInput() && !stopping_) {
|
||||||
|
if (threw && connected_.count(item.id)) {
|
||||||
|
QueueReleaseLocked(item.id);
|
||||||
|
} else if (outcome == InputOutcome::failed) {
|
||||||
|
if (const unsigned mask = MouseUpMask(*item.action))
|
||||||
|
QueueMouseReleaseLocked(item.id, mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool report =
|
||||||
|
ShouldReportLocked(item, finished, queued_ms, execution_ms, outcome);
|
||||||
|
if (!connected_.count(item.id) && !blocked_.count(item.id) &&
|
||||||
|
!pending_counts_.count(item.id))
|
||||||
|
logs_.erase(item.id);
|
||||||
|
lock.unlock();
|
||||||
|
if (report) {
|
||||||
|
try {
|
||||||
|
report_(item.id, queued_ms, execution_ms, outcome);
|
||||||
|
} catch (...) {
|
||||||
|
} // Diagnostics must not terminate the input worker.
|
||||||
|
}
|
||||||
|
lock.lock();
|
||||||
|
executing_ = false;
|
||||||
|
if (pending_.empty()) idle_.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-10-22
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _REMOTE_INPUT_DISPATCHER_H_
|
||||||
|
#define _REMOTE_INPUT_DISPATCHER_H_
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include "input_outcome.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// Serializes native input, recovery and display resets off the RTC thread.
|
||||||
|
class RemoteInputDispatcher {
|
||||||
|
public:
|
||||||
|
using Apply =
|
||||||
|
std::function<InputOutcome(const std::string&, const RemoteAction&)>;
|
||||||
|
using Release = std::function<InputOutcome(const std::string&)>;
|
||||||
|
using Report =
|
||||||
|
std::function<void(const std::string&, int64_t, int64_t, InputOutcome)>;
|
||||||
|
using ResetDisplay = std::function<InputOutcome()>;
|
||||||
|
// Optional targeted adapter; legacy callers fall back to full Release.
|
||||||
|
using ReleaseMouse =
|
||||||
|
std::function<InputOutcome(const std::string&, unsigned)>;
|
||||||
|
enum class Result { queued, coalesced, rejected, overflow };
|
||||||
|
static constexpr size_t kMaxPendingPerController = 128;
|
||||||
|
|
||||||
|
RemoteInputDispatcher(Apply apply, Release release, Report report = {},
|
||||||
|
ResetDisplay reset_display = {},
|
||||||
|
ReleaseMouse release_mouse = {});
|
||||||
|
~RemoteInputDispatcher();
|
||||||
|
static bool Accepts(const RemoteAction& action);
|
||||||
|
void Connect(const std::string& id);
|
||||||
|
Result Push(const std::string& id, const RemoteAction& action);
|
||||||
|
void Disconnect(const std::string& id);
|
||||||
|
|
||||||
|
// Scheduled recovery is not idle work. Check HasPendingReleases() as well,
|
||||||
|
// or Stop() before destroying the devices used by callbacks.
|
||||||
|
void WaitIdle();
|
||||||
|
bool HasPendingReleases();
|
||||||
|
// Cancels pending input/timers and makes a final bounded cleanup attempt.
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
enum class ItemKind { input, cleanup, reset_display };
|
||||||
|
struct Item {
|
||||||
|
std::string id;
|
||||||
|
std::optional<RemoteAction> action; // Present only for ordinary input.
|
||||||
|
Clock::time_point queued_at;
|
||||||
|
ItemKind kind = ItemKind::cleanup;
|
||||||
|
// Zero requests full controller cleanup; otherwise only these mouse
|
||||||
|
// buttons.
|
||||||
|
unsigned mouse_mask = 0;
|
||||||
|
bool IsReset() const { return kind == ItemKind::reset_display; }
|
||||||
|
bool IsInput() const { return kind == ItemKind::input; }
|
||||||
|
bool IsCleanup() const { return kind == ItemKind::cleanup; }
|
||||||
|
};
|
||||||
|
struct Retry {
|
||||||
|
Clock::time_point due;
|
||||||
|
std::chrono::milliseconds delay{100};
|
||||||
|
unsigned mouse_mask = 0;
|
||||||
|
};
|
||||||
|
struct LogState {
|
||||||
|
Clock::time_point slow{};
|
||||||
|
std::array<Clock::time_point, 2> failure{};
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool IsMove(const RemoteAction& action);
|
||||||
|
static unsigned MouseUpMask(const RemoteAction& action);
|
||||||
|
void RemoveResetsLocked();
|
||||||
|
void QueueReleaseLocked(const std::string& id);
|
||||||
|
void QueueMouseReleaseLocked(const std::string& id, unsigned mask);
|
||||||
|
void RemoveQueuedCleanupLocked(const std::string& id);
|
||||||
|
void ScheduleRetryLocked(const std::string& id, unsigned mouse_mask,
|
||||||
|
Clock::time_point finished);
|
||||||
|
void CompleteCleanupLocked(const Item& item, InputOutcome outcome,
|
||||||
|
Clock::time_point finished);
|
||||||
|
std::optional<Item> NextLocked();
|
||||||
|
InputOutcome Execute(const Item& item);
|
||||||
|
bool ShouldReportLocked(const Item& item, Clock::time_point now,
|
||||||
|
int64_t queued_ms, int64_t execution_ms,
|
||||||
|
InputOutcome outcome);
|
||||||
|
void Run();
|
||||||
|
|
||||||
|
Apply apply_;
|
||||||
|
Release release_;
|
||||||
|
Report report_;
|
||||||
|
ResetDisplay reset_display_;
|
||||||
|
ReleaseMouse release_mouse_;
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::condition_variable ready_, idle_;
|
||||||
|
std::deque<Item> pending_;
|
||||||
|
std::unordered_set<std::string> connected_, blocked_;
|
||||||
|
std::unordered_map<std::string, size_t> pending_counts_;
|
||||||
|
std::unordered_map<std::string, Retry> retries_;
|
||||||
|
std::unordered_map<std::string, LogState> logs_;
|
||||||
|
bool last_was_retry_ = false;
|
||||||
|
bool stopping_ = false;
|
||||||
|
bool executing_ = false;
|
||||||
|
std::once_flag stop_once_;
|
||||||
|
std::thread worker_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-10-22
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _REMOTE_MOUSE_BUTTONS_H_
|
||||||
|
#define _REMOTE_MOUSE_BUTTONS_H_
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "input_outcome.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// One physical button cannot belong to two controllers at once. A second
|
||||||
|
// controller's down/up must not steal or release an existing drag.
|
||||||
|
class RemoteMouseButtons {
|
||||||
|
public:
|
||||||
|
template <typename Send>
|
||||||
|
InputOutcome Apply(const std::string& id, MouseFlag flag, Send send) {
|
||||||
|
if (id.empty()) return InputOutcome::ignored;
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
const int value = static_cast<int>(flag);
|
||||||
|
if (value < 1 || value > 6) return send();
|
||||||
|
auto& owner = owners_[(value - 1) / 2];
|
||||||
|
const bool down = value % 2 == 1;
|
||||||
|
if ((!owner.empty() && owner != id) || (!down && owner.empty()))
|
||||||
|
return InputOutcome::ignored;
|
||||||
|
// A service timeout may follow successful injection. Retain an attempted
|
||||||
|
// down until an up or explicit cleanup is confirmed, including exceptions.
|
||||||
|
if (down) owner = id;
|
||||||
|
const auto outcome = send();
|
||||||
|
if (!down && outcome == InputOutcome::applied) owner.clear();
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Release>
|
||||||
|
bool ReleaseController(const std::string& id, Release release,
|
||||||
|
unsigned requested_mask = 7) {
|
||||||
|
if (id.empty()) return true;
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
unsigned mask = 0;
|
||||||
|
for (size_t i = 0; i < owners_.size(); ++i)
|
||||||
|
if (owners_[i] == id && (requested_mask & (1u << i))) mask |= 1u << i;
|
||||||
|
return ReleaseLocked(mask, release);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Release>
|
||||||
|
bool ReleaseAll(Release release) {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
// Also clear any backend state retained after a desktop/route transition.
|
||||||
|
return ReleaseLocked(7, release);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename Release>
|
||||||
|
bool ReleaseLocked(unsigned mask, Release release) {
|
||||||
|
if (!mask) return true;
|
||||||
|
// The sink returns the subset confirmed released, so one failed button
|
||||||
|
// does not retain ownership of other successfully released buttons.
|
||||||
|
const unsigned released = release(mask) & mask;
|
||||||
|
for (size_t i = 0; i < owners_.size(); ++i)
|
||||||
|
if (released & (1u << i)) owners_[i].clear();
|
||||||
|
return released == mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::array<std::string, 3> owners_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -190,20 +190,13 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
|||||||
const std::string& remote_id, const char* reason) {
|
const std::string& remote_id, const char* reason) {
|
||||||
transfers_.Cancel(peer_events_.get(), remote_id);
|
transfers_.Cancel(peer_events_.get(), remote_id);
|
||||||
preview_subscriptions_.Remove(remote_id);
|
preview_subscriptions_.Remove(remote_id);
|
||||||
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
|
|
||||||
#ifdef _WIN32
|
|
||||||
devices_.ReleaseRemoteMouseButtons();
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
std::lock_guard lock(remote_pointer_input_mutex_);
|
|
||||||
last_remote_pointer_input_time_.erase(remote_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool has_connected_controller = false;
|
bool has_connected_controller = false;
|
||||||
std::string remaining_controller_id;
|
std::string remaining_controller_id;
|
||||||
{
|
{
|
||||||
std::unique_lock lock(connection_status_mutex_);
|
std::unique_lock lock(connection_status_mutex_);
|
||||||
connection_status_.erase(remote_id);
|
connection_status_.erase(remote_id);
|
||||||
|
remote_input_.Disconnect(remote_id);
|
||||||
devices_.OnControllerConnectionsChanged();
|
devices_.OnControllerConnectionsChanged();
|
||||||
connection_host_names_.erase(remote_id);
|
connection_host_names_.erase(remote_id);
|
||||||
privacy_sessions_.Disconnected(remote_id);
|
privacy_sessions_.Disconnected(remote_id);
|
||||||
@@ -252,7 +245,6 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
|||||||
start_keyboard_capturer_ = false;
|
start_keyboard_capturer_ = false;
|
||||||
remote_client_id_.clear();
|
remote_client_id_.clear();
|
||||||
audio_capture_ = false;
|
audio_capture_ = false;
|
||||||
devices_.ResetToInitialDisplay();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
|
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "platform/video_renderer.h"
|
#include "platform/video_renderer.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include "platform/windows/input/windows_input_injector.h"
|
||||||
|
#endif
|
||||||
#if defined(__linux__) && !defined(__OHOS__)
|
#if defined(__linux__) && !defined(__OHOS__)
|
||||||
#include "platform/linux/headless/headless_console.h"
|
#include "platform/linux/headless/headless_console.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -25,9 +28,43 @@ namespace crossdesk {
|
|||||||
|
|
||||||
GuiRuntime::GuiRuntime()
|
GuiRuntime::GuiRuntime()
|
||||||
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
|
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
|
||||||
keyboard_(*this) {}
|
keyboard_(*this),
|
||||||
|
remote_input_(
|
||||||
|
[this](const std::string& id, const RemoteAction& action) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
ScopedUserDesktopInputThread input_thread;
|
||||||
|
#endif
|
||||||
|
return ApplyRemoteInput(id, action);
|
||||||
|
},
|
||||||
|
[this](const std::string& id) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
ScopedUserDesktopInputThread input_thread;
|
||||||
|
#endif
|
||||||
|
const bool keys_released =
|
||||||
|
keyboard_.ReleaseRemotePressedKeys(id, "input_queue_reset");
|
||||||
|
const bool mouse_released = devices_.ReleaseRemoteMouseButtons(id);
|
||||||
|
std::lock_guard lock(remote_pointer_input_mutex_);
|
||||||
|
last_remote_pointer_input_time_.erase(id);
|
||||||
|
return keys_released && mouse_released ? InputOutcome::applied
|
||||||
|
: InputOutcome::failed;
|
||||||
|
},
|
||||||
|
[](const std::string& id, int64_t queued_ms, int64_t execution_ms,
|
||||||
|
InputOutcome outcome) {
|
||||||
|
LOG_WARN("Remote input result: peer={} queue_ms={} execution_ms={} "
|
||||||
|
"result={}",
|
||||||
|
id, queued_ms, execution_ms, InputOutcomeName(outcome));
|
||||||
|
},
|
||||||
|
[this] { return devices_.ResetToInitialDisplay(); },
|
||||||
|
[this](const std::string& id, unsigned mask) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
ScopedUserDesktopInputThread input_thread;
|
||||||
|
#endif
|
||||||
|
return devices_.ReleaseRemoteMouseButtons(id, mask)
|
||||||
|
? InputOutcome::applied : InputOutcome::failed;
|
||||||
|
}) {}
|
||||||
|
|
||||||
GuiRuntime::~GuiRuntime() {
|
GuiRuntime::~GuiRuntime() {
|
||||||
|
remote_input_.Stop();
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed);
|
windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed);
|
||||||
windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed);
|
windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "features/devices/session_device_manager.h"
|
#include "features/devices/session_device_manager.h"
|
||||||
#include "features/file_transfer/file_transfer_manager.h"
|
#include "features/file_transfer/file_transfer_manager.h"
|
||||||
#include "features/input/keyboard_controller.h"
|
#include "features/input/keyboard_controller.h"
|
||||||
|
#include "features/input/remote_input_dispatcher.h"
|
||||||
#include "features/settings/settings_manager.h"
|
#include "features/settings/settings_manager.h"
|
||||||
#include "privacy_controller.h"
|
#include "privacy_controller.h"
|
||||||
#include "runtime/gui_state.h"
|
#include "runtime/gui_state.h"
|
||||||
@@ -90,6 +91,8 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
// UI thread: send mapped mouse/keyboard input to the current control target,
|
// UI thread: send mapped mouse/keyboard input to the current control target,
|
||||||
// or every connected session while that target leads DeskHub group control.
|
// or every connected session while that target leads DeskHub group control.
|
||||||
void SendRemoteInput(const RemoteAction& action);
|
void SendRemoteInput(const RemoteAction& action);
|
||||||
|
InputOutcome ApplyRemoteInput(const std::string& remote_id, const RemoteAction& action);
|
||||||
|
InputOutcome InjectRemoteMouse(const RemoteAction& action, int display);
|
||||||
std::string gallery_group_control_id_;
|
std::string gallery_group_control_id_;
|
||||||
|
|
||||||
void ResetRemoteServiceStatus(RemoteSession& props);
|
void ResetRemoteServiceStatus(RemoteSession& props);
|
||||||
@@ -133,6 +136,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
FileTransferManager transfers_;
|
FileTransferManager transfers_;
|
||||||
SettingsManager settings_;
|
SettingsManager settings_;
|
||||||
KeyboardController keyboard_;
|
KeyboardController keyboard_;
|
||||||
|
RemoteInputDispatcher remote_input_;
|
||||||
std::shared_ptr<PeerEventHandler> peer_events_;
|
std::shared_ptr<PeerEventHandler> peer_events_;
|
||||||
std::atomic<bool> video_frame_dirty_{false};
|
std::atomic<bool> video_frame_dirty_{false};
|
||||||
std::unique_ptr<VideoRenderer> video_renderer_;
|
std::unique_ptr<VideoRenderer> video_renderer_;
|
||||||
|
|||||||
@@ -280,29 +280,22 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
runtime->QueuePrivacyCommand(remote_id, remote_action.pc);
|
runtime->QueuePrivacyCommand(remote_id, remote_action.pc);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (remote_action.type == ControlType::text_input) {
|
if (RemoteInputDispatcher::Accepts(remote_action)) {
|
||||||
// Share the ordered keyboard stream with delete/return and shortcuts.
|
if (!runtime->IsAuthorizedController(remote_id)) return;
|
||||||
if (source_id == runtime->keyboard_label_ &&
|
if (remote_action.type == ControlType::text_input &&
|
||||||
runtime->IsAuthorizedController(remote_id))
|
source_id != runtime->keyboard_label_) return;
|
||||||
runtime->keyboard_.ApplyRemoteText(remote_id, remote_action.text);
|
if (runtime->remote_input_.Push(remote_id, remote_action) ==
|
||||||
|
RemoteInputDispatcher::Result::overflow)
|
||||||
|
LOG_WARN("Remote input queue overflow: peer={}, clearing stale input and "
|
||||||
|
"releasing keys/buttons",
|
||||||
|
remote_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bool operates_host = remote_action.type == ControlType::mouse ||
|
if (remote_action.type == ControlType::audio_capture) {
|
||||||
remote_action.type == ControlType::keyboard || remote_action.type == ControlType::keyboard_state ||
|
|
||||||
remote_action.type == ControlType::service_command || remote_action.type == ControlType::display_id ||
|
|
||||||
remote_action.type == ControlType::audio_capture;
|
|
||||||
if (operates_host) {
|
|
||||||
if (!runtime->IsAuthorizedController(remote_id)) return;
|
if (!runtime->IsAuthorizedController(remote_id)) return;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// KeyboardController checks the desktop immediately before each native
|
if (runtime->privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable())
|
||||||
// key injection, including state reconciliation and cleanup. Avoid a
|
|
||||||
// second synchronous session query for every received keyboard packet.
|
|
||||||
const bool keyboard_input = remote_action.type == ControlType::keyboard ||
|
|
||||||
remote_action.type == ControlType::keyboard_state;
|
|
||||||
if (!keyboard_input && runtime->privacy_.Engaged() &&
|
|
||||||
!IsWindowsPrivacyDesktopAvailable()) {
|
|
||||||
runtime->privacy_.SuspendForDesktop();
|
runtime->privacy_.SuspendForDesktop();
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
if (remote_action.type == ControlType::service_status) {
|
if (remote_action.type == ControlType::service_status) {
|
||||||
@@ -312,28 +305,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remote_action.type == ControlType::service_command) {
|
|
||||||
if (runtime->privacy_.Engaged()) {
|
|
||||||
runtime->privacy_.Fail("Security shortcut requested; privacy screen will turn off");
|
|
||||||
}
|
|
||||||
#if _WIN32
|
|
||||||
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
|
|
||||||
runtime->pending_windows_service_sas_.store(true,
|
|
||||||
std::memory_order_relaxed);
|
|
||||||
} else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) {
|
|
||||||
if (!LockWorkStation()) {
|
|
||||||
LOG_WARN("Remote lock workstation request failed, error={}",
|
|
||||||
GetLastError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#elif defined(__OHOS__)
|
|
||||||
if (!SendHarmonySystemShortcut(remote_action.c.flag))
|
|
||||||
LOG_WARN("Harmony system shortcut unsupported or unauthorized, command={}",
|
|
||||||
static_cast<int>(remote_action.c.flag));
|
|
||||||
#endif
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remote_action.type == ControlType::cursor_state) {
|
if (remote_action.type == ControlType::cursor_state) {
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||||
{
|
{
|
||||||
@@ -464,75 +435,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
FreeRemoteAction(remote_action);
|
FreeRemoteAction(remote_action);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// remote
|
if (remote_action.type == ControlType::audio_capture) {
|
||||||
if (runtime->is_server_mode_ &&
|
|
||||||
remote_action.type == ControlType::mouse) {
|
|
||||||
std::lock_guard lock(runtime->remote_pointer_input_mutex_);
|
|
||||||
runtime->last_remote_pointer_input_time_[remote_id] =
|
|
||||||
std::chrono::steady_clock::now();
|
|
||||||
}
|
|
||||||
#if _WIN32
|
|
||||||
if (runtime->is_server_mode_ && remote_action.type == ControlType::mouse) {
|
|
||||||
static const bool process_elevated = IsAdministratorProcess();
|
|
||||||
std::string response;
|
|
||||||
const bool delivered = DispatchDesktopInput(
|
|
||||||
[&] {
|
|
||||||
return PreferUserDesktopInput(
|
|
||||||
IsCurrentSessionUserDesktopActive(),
|
|
||||||
runtime->windows_consent_ui_.load(),
|
|
||||||
runtime->local_service_available_.load(), process_elevated);
|
|
||||||
},
|
|
||||||
[&] {
|
|
||||||
SetLastError(ERROR_SUCCESS);
|
|
||||||
const bool sent = runtime->devices_.SendMouseCommand(
|
|
||||||
remote_action, runtime->selected_display_);
|
|
||||||
const DWORD error = GetLastError();
|
|
||||||
const bool blocked =
|
|
||||||
error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE;
|
|
||||||
return DesktopInputResult{
|
|
||||||
sent, IsDesktopTransitionInputError(error) || blocked, blocked};
|
|
||||||
},
|
|
||||||
[&]() -> DesktopInputResult {
|
|
||||||
int absolute_x = 0;
|
|
||||||
int absolute_y = 0;
|
|
||||||
if (!BuildAbsoluteMousePosition(
|
|
||||||
runtime->devices_.display_info_list(),
|
|
||||||
runtime->selected_display_, remote_action.m.x,
|
|
||||||
remote_action.m.y, &absolute_x, &absolute_y)) {
|
|
||||||
LOG_WARN(
|
|
||||||
"Secure desktop mouse injection skipped, invalid display "
|
|
||||||
"mapping: display_index={}, x={}, y={}",
|
|
||||||
runtime->selected_display_, remote_action.m.x,
|
|
||||||
remote_action.m.y);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
response = runtime->devices_.SendServiceMouseCommand(
|
|
||||||
absolute_x, absolute_y, remote_action.m.s,
|
|
||||||
static_cast<int>(remote_action.m.flag));
|
|
||||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
|
||||||
const bool sent = json.is_object() && json.value("ok", false);
|
|
||||||
const std::string error =
|
|
||||||
json.is_object() ? json.value("error", "") : "";
|
|
||||||
return {sent, IsDesktopInputSetupPending(error)};
|
|
||||||
});
|
|
||||||
if (!delivered && !response.empty()) {
|
|
||||||
LogSecureDesktopInputBlocked(
|
|
||||||
&runtime->last_local_secure_input_block_log_tick_, "local",
|
|
||||||
runtime->WindowsInputStage().c_str());
|
|
||||||
LOG_WARN(
|
|
||||||
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
|
|
||||||
"flag={}, response={}",
|
|
||||||
remote_action.m.x, remote_action.m.y, remote_action.m.s,
|
|
||||||
static_cast<int>(remote_action.m.flag), response);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (remote_action.type == ControlType::mouse) {
|
|
||||||
runtime->devices_.SendMouseCommand(remote_action,
|
|
||||||
runtime->selected_display_);
|
|
||||||
} else if (remote_action.type == ControlType::audio_capture) {
|
|
||||||
// A negotiated subscription remains authoritative if a legacy capture
|
// A negotiated subscription remains authoritative if a legacy capture
|
||||||
// command arrives after it (e.g. when unmuting an inactive window).
|
// command arrives after it (e.g. when unmuting an inactive window).
|
||||||
const auto subscription = runtime->preview_subscriptions_.Get(remote_id);
|
const auto subscription = runtime->preview_subscriptions_.Get(remote_id);
|
||||||
@@ -544,20 +447,139 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
else if (ret != 0)
|
else if (ret != 0)
|
||||||
// Legacy/web transports without the per-peer API retain their fallback.
|
// Legacy/web transports without the per-peer API retain their fallback.
|
||||||
runtime->devices_.StopSpeakerCapturer();
|
runtime->devices_.StopSpeakerCapturer();
|
||||||
} else if (remote_action.type == ControlType::keyboard) {
|
|
||||||
runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action);
|
|
||||||
} else if (remote_action.type == ControlType::keyboard_state) {
|
|
||||||
runtime->keyboard_.ApplyRemoteState(remote_id, remote_action);
|
|
||||||
} else if (remote_action.type == ControlType::display_id) {
|
|
||||||
const int ret = runtime->devices_.SwitchDisplay(remote_action.d);
|
|
||||||
if (ret == 0) {
|
|
||||||
runtime->selected_display_ = remote_action.d;
|
|
||||||
} else {
|
|
||||||
LOG_WARN("Display switch skipped, invalid display_id={}",
|
|
||||||
remote_action.d);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
InputOutcome GuiRuntime::ApplyRemoteInput(const std::string& remote_id,
|
||||||
|
const RemoteAction& remote_action) {
|
||||||
|
if (!IsAuthorizedController(remote_id)) return InputOutcome::ignored;
|
||||||
|
if (remote_action.type == ControlType::text_input) {
|
||||||
|
return keyboard_.ApplyRemoteText(remote_id, remote_action.text);
|
||||||
|
}
|
||||||
|
#ifdef _WIN32
|
||||||
|
const bool keyboard_input = remote_action.type == ControlType::keyboard ||
|
||||||
|
remote_action.type == ControlType::keyboard_state;
|
||||||
|
if (!keyboard_input && privacy_.Engaged() &&
|
||||||
|
!IsWindowsPrivacyDesktopAvailable())
|
||||||
|
privacy_.SuspendForDesktop();
|
||||||
|
#endif
|
||||||
|
if (remote_action.type == ControlType::service_command) {
|
||||||
|
if (privacy_.Engaged()) {
|
||||||
|
privacy_.Fail(
|
||||||
|
"Security shortcut requested; privacy screen will turn off");
|
||||||
|
}
|
||||||
|
#if _WIN32
|
||||||
|
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
|
||||||
|
pending_windows_service_sas_.store(true, std::memory_order_relaxed);
|
||||||
|
return InputOutcome::applied;
|
||||||
|
} else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) {
|
||||||
|
if (!LockWorkStation()) {
|
||||||
|
LOG_WARN("Remote lock workstation request failed, error={}",
|
||||||
|
GetLastError());
|
||||||
|
return InputOutcome::failed;
|
||||||
|
}
|
||||||
|
return InputOutcome::applied;
|
||||||
|
}
|
||||||
|
#elif defined(__OHOS__)
|
||||||
|
if (!SendHarmonySystemShortcut(remote_action.c.flag)) {
|
||||||
|
LOG_WARN(
|
||||||
|
"Harmony system shortcut unsupported or unauthorized, command={}",
|
||||||
|
static_cast<int>(remote_action.c.flag));
|
||||||
|
return InputOutcome::failed;
|
||||||
|
}
|
||||||
|
return InputOutcome::applied;
|
||||||
|
#endif
|
||||||
|
return InputOutcome::ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int display = selected_display_.load();
|
||||||
|
if (is_server_mode_ && remote_action.type == ControlType::mouse) {
|
||||||
|
std::lock_guard lock(remote_pointer_input_mutex_);
|
||||||
|
last_remote_pointer_input_time_[remote_id] =
|
||||||
|
std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remote_action.type == ControlType::mouse) {
|
||||||
|
return devices_.ApplyRemoteMouse(remote_id, remote_action.m.flag,
|
||||||
|
[this, &remote_action, display] {
|
||||||
|
return InjectRemoteMouse(remote_action, display);
|
||||||
|
});
|
||||||
|
} else if (remote_action.type == ControlType::keyboard) {
|
||||||
|
return keyboard_.ApplyRemoteEvent(remote_id, remote_action);
|
||||||
|
} else if (remote_action.type == ControlType::keyboard_state) {
|
||||||
|
return keyboard_.ApplyRemoteState(remote_id, remote_action);
|
||||||
|
} else if (remote_action.type == ControlType::display_id) {
|
||||||
|
const int ret = devices_.SwitchDisplay(remote_action.d);
|
||||||
|
if (ret == 0) {
|
||||||
|
selected_display_ = remote_action.d;
|
||||||
|
return InputOutcome::applied;
|
||||||
|
} else {
|
||||||
|
LOG_WARN("Display switch skipped, invalid display_id={}",
|
||||||
|
remote_action.d);
|
||||||
|
return InputOutcome::failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return InputOutcome::ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputOutcome GuiRuntime::InjectRemoteMouse(const RemoteAction& remote_action,
|
||||||
|
int display) {
|
||||||
|
#if _WIN32
|
||||||
|
if (is_server_mode_) {
|
||||||
|
static const bool process_elevated = IsAdministratorProcess();
|
||||||
|
std::string response;
|
||||||
|
const bool delivered = DispatchDesktopInput(
|
||||||
|
[&] {
|
||||||
|
return PreferUserDesktopInput(
|
||||||
|
IsCurrentSessionUserDesktopActive(), windows_consent_ui_.load(),
|
||||||
|
local_service_available_.load(), process_elevated);
|
||||||
|
},
|
||||||
|
[&] {
|
||||||
|
SetLastError(ERROR_SUCCESS);
|
||||||
|
const bool sent = devices_.SendMouseCommand(remote_action, display);
|
||||||
|
const DWORD error = GetLastError();
|
||||||
|
const bool blocked =
|
||||||
|
error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE;
|
||||||
|
return DesktopInputResult{
|
||||||
|
sent, IsDesktopTransitionInputError(error) || blocked, blocked};
|
||||||
|
},
|
||||||
|
[&]() -> DesktopInputResult {
|
||||||
|
int absolute_x = 0;
|
||||||
|
int absolute_y = 0;
|
||||||
|
if (!BuildAbsoluteMousePosition(devices_.display_info_list(), display,
|
||||||
|
remote_action.m.x, remote_action.m.y,
|
||||||
|
&absolute_x, &absolute_y)) {
|
||||||
|
LOG_WARN(
|
||||||
|
"Secure desktop mouse injection skipped, invalid display "
|
||||||
|
"mapping: display_index={}, x={}, y={}",
|
||||||
|
display, remote_action.m.x, remote_action.m.y);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
response = devices_.SendServiceMouseCommand(
|
||||||
|
absolute_x, absolute_y, remote_action.m.s,
|
||||||
|
static_cast<int>(remote_action.m.flag));
|
||||||
|
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||||
|
const bool sent = json.is_object() && json.value("ok", false);
|
||||||
|
const std::string error =
|
||||||
|
json.is_object() ? json.value("error", "") : "";
|
||||||
|
return {sent, IsDesktopInputSetupPending(error)};
|
||||||
|
});
|
||||||
|
if (!delivered && !response.empty()) {
|
||||||
|
LogSecureDesktopInputBlocked(&last_local_secure_input_block_log_tick_,
|
||||||
|
"local", WindowsInputStage().c_str());
|
||||||
|
LOG_WARN(
|
||||||
|
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
|
||||||
|
"flag={}, response={}",
|
||||||
|
remote_action.m.x, remote_action.m.y, remote_action.m.s,
|
||||||
|
static_cast<int>(remote_action.m.flag), response);
|
||||||
|
}
|
||||||
|
return delivered ? InputOutcome::applied : InputOutcome::failed;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return devices_.SendMouseCommand(remote_action, display)
|
||||||
|
? InputOutcome::applied : InputOutcome::failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -437,6 +437,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
runtime->ApplyPrivacyAdmission(automatic_enable);
|
runtime->ApplyPrivacyAdmission(automatic_enable);
|
||||||
}
|
}
|
||||||
runtime->connection_status_[remote_id] = status;
|
runtime->connection_status_[remote_id] = status;
|
||||||
|
if (status == ConnectionStatus::Connected)
|
||||||
|
runtime->remote_input_.Connect(remote_id);
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
runtime->windows_rdp_handoff_connected_->store(std::any_of(
|
runtime->windows_rdp_handoff_connected_->store(std::any_of(
|
||||||
runtime->connection_status_.begin(), runtime->connection_status_.end(),
|
runtime->connection_status_.begin(), runtime->connection_status_.end(),
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ int GuiApplication::RunUnattended(void* stop_event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Quiesce network callbacks before destroying the controllers they use.
|
// Quiesce network callbacks before destroying the controllers they use.
|
||||||
|
remote_input_.Stop();
|
||||||
CloseAllRemoteSessions();
|
CloseAllRemoteSessions();
|
||||||
WaitForSessionCleanup();
|
WaitForSessionCleanup();
|
||||||
keyboard_.ReleaseAllRemotePressedKeys("unattended_host_exit");
|
keyboard_.ReleaseAllRemotePressedKeys("unattended_host_exit");
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace {
|
|||||||
struct InputResult {
|
struct InputResult {
|
||||||
UINT sent;
|
UINT sent;
|
||||||
DWORD error;
|
DWORD error;
|
||||||
|
bool requires_worker = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
InputResult InjectOnUserDesktop(INPUT* inputs, UINT count) {
|
InputResult InjectOnUserDesktop(INPUT* inputs, UINT count) {
|
||||||
@@ -25,7 +26,10 @@ InputResult InjectOnUserDesktop(INPUT* inputs, UINT count) {
|
|||||||
ScopedInteractiveDesktop desktop;
|
ScopedInteractiveDesktop desktop;
|
||||||
if (!desktop.Bind(L"")) {
|
if (!desktop.Bind(L"")) {
|
||||||
const DWORD error = GetLastError();
|
const DWORD error = GetLastError();
|
||||||
return {0, error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE};
|
// A library may have created a window/hook on an opted-in thread. This
|
||||||
|
// failure occurs before SendInput, so retrying on the worker is safe.
|
||||||
|
return {0, error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE,
|
||||||
|
error == ERROR_BUSY};
|
||||||
}
|
}
|
||||||
if (!desktop.IsReceivingInput()) {
|
if (!desktop.IsReceivingInput()) {
|
||||||
return {0, ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION};
|
return {0, ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION};
|
||||||
@@ -99,8 +103,13 @@ UINT SendInputOnUserDesktop(INPUT* inputs, UINT count) {
|
|||||||
SetLastError(ERROR_INVALID_PARAMETER);
|
SetLastError(ERROR_INVALID_PARAMETER);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
static UserDesktopInputWorker worker;
|
InputResult result{0, ERROR_SUCCESS, true};
|
||||||
const InputResult result = worker.Send(inputs, count);
|
if (ScopedUserDesktopInputThread::IsCurrent())
|
||||||
|
result = InjectOnUserDesktop(inputs, count);
|
||||||
|
if (result.requires_worker) {
|
||||||
|
static UserDesktopInputWorker worker;
|
||||||
|
result = worker.Send(inputs, count);
|
||||||
|
}
|
||||||
const DWORD error = result.error;
|
const DWORD error = result.error;
|
||||||
if (error != ERROR_SUCCESS) {
|
if (error != ERROR_SUCCESS) {
|
||||||
// Include session identity even when Windows reports no useful SendInput
|
// Include session identity even when Windows reports no useful SendInput
|
||||||
|
|||||||
@@ -11,9 +11,23 @@
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
// Executes on a worker without windows or hooks, attached to this process's
|
// Opt in only on the remote input worker. UI/hook callers keep using the
|
||||||
// current user input desktop. Returns a SendInput count and preserves the
|
// fallback worker. Desktop attachment is still checked for each injection.
|
||||||
// worker's failure code in the calling thread's last-error value.
|
class ScopedUserDesktopInputThread {
|
||||||
|
public:
|
||||||
|
ScopedUserDesktopInputThread() { ++depth_; }
|
||||||
|
~ScopedUserDesktopInputThread() { --depth_; }
|
||||||
|
ScopedUserDesktopInputThread(const ScopedUserDesktopInputThread&) = delete;
|
||||||
|
ScopedUserDesktopInputThread& operator=(const ScopedUserDesktopInputThread&) =
|
||||||
|
delete;
|
||||||
|
static bool IsCurrent() { return depth_ != 0; }
|
||||||
|
private:
|
||||||
|
inline static thread_local unsigned depth_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Executes directly on an opted-in worker, or on the fallback input worker,
|
||||||
|
// attached to this process's current user input desktop. Returns a SendInput
|
||||||
|
// count and preserves the failure code in the calling thread's last-error value.
|
||||||
UINT SendInputOnUserDesktop(const INPUT& input);
|
UINT SendInputOnUserDesktop(const INPUT& input);
|
||||||
|
|
||||||
// Synchronously injects one ordered batch. The caller owns the buffer until
|
// Synchronously injects one ordered batch. The caller owns the buffer until
|
||||||
|
|||||||
Reference in New Issue
Block a user