mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 08:19:44 +08:00
573 lines
24 KiB
C++
573 lines
24 KiB
C++
#include <SDL3/SDL_timer.h>
|
|
|
|
#include "runtime/peer_event_handler.h"
|
|
|
|
#include <app_version.h>
|
|
#include <remote_action.h>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <shared_mutex>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
#include "device_controller.h"
|
|
#include "file_transfer.h"
|
|
#include "localization.h"
|
|
#include "filesystem_utf8.h"
|
|
#include "platform.h"
|
|
#include "rd_log.h"
|
|
#include "runtime/gui_runtime.h"
|
|
|
|
#if _WIN32
|
|
#include "desktop_transition_policy.h"
|
|
#include "interactive_desktop.h"
|
|
#include "service_host.h"
|
|
#include "unattended_config.h"
|
|
#endif
|
|
|
|
namespace crossdesk {
|
|
|
|
namespace {
|
|
#if _WIN32
|
|
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
|
|
|
bool BuildAbsoluteMousePosition(const std::vector<DisplayInfo> &displays,
|
|
int display_index, float normalized_x,
|
|
float normalized_y, int *absolute_x_out,
|
|
int *absolute_y_out) {
|
|
if (absolute_x_out == nullptr || absolute_y_out == nullptr ||
|
|
display_index < 0 || display_index >= static_cast<int>(displays.size())) {
|
|
return false;
|
|
}
|
|
|
|
const DisplayInfo &display = displays[display_index];
|
|
if (display.width <= 0 || display.height <= 0) {
|
|
return false;
|
|
}
|
|
|
|
const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f);
|
|
const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f);
|
|
*absolute_x_out = static_cast<int>(clamped_x * display.width) + display.left;
|
|
*absolute_y_out = static_cast<int>(clamped_y * display.height) + display.top;
|
|
return true;
|
|
}
|
|
|
|
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side,
|
|
const char *stage) {
|
|
if (last_tick == nullptr) {
|
|
return;
|
|
}
|
|
|
|
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
|
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
|
return;
|
|
}
|
|
|
|
*last_tick = now;
|
|
LOG_WARN("{} secure-desktop input blocked, stage={}, normal SendInput path "
|
|
"cannot drive the Windows password UI",
|
|
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
|
|
}
|
|
|
|
#endif
|
|
|
|
} // namespace
|
|
|
|
void PeerEventHandler::OnReceiveDataBuffer(
|
|
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
|
const char *src_id, size_t src_id_size, void *user_data) {
|
|
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
|
if (!handler) return;
|
|
auto callback = handler->EnterCallback();
|
|
if (!callback) return;
|
|
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
|
if (!runtime) {
|
|
return;
|
|
}
|
|
|
|
std::string source_id = std::string(src_id, src_id_size);
|
|
// The service-owned host runs as SYSTEM. Do not expose filesystem or
|
|
// clipboard operations in that security context.
|
|
if (runtime->unattended_host_ &&
|
|
(source_id == runtime->file_label_ ||
|
|
source_id == runtime->file_feedback_label_ ||
|
|
source_id == runtime->clipboard_label_))
|
|
return;
|
|
if (source_id == runtime->file_label_) {
|
|
std::string remote_user_id = std::string(user_id, user_id_size);
|
|
|
|
static FileReceiver receiver;
|
|
// Update output directory from config
|
|
std::string configured_path =
|
|
runtime->config_center_->GetFileTransferSavePath();
|
|
if (!configured_path.empty()) {
|
|
receiver.SetOutputDir(PathFromUtf8(configured_path));
|
|
} else if (receiver.OutputDir().empty()) {
|
|
receiver = FileReceiver(); // re-init with default desktop path
|
|
}
|
|
receiver.SetOnSendAck([runtime,
|
|
remote_user_id](const FileTransferAck &ack) -> int {
|
|
const auto encoded_ack = EncodeFileTransferAck(ack);
|
|
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
|
|
if (is_server_sending) {
|
|
auto props =
|
|
runtime->FindRemoteSession(remote_user_id);
|
|
if (props) {
|
|
PeerPtr *peer = props->peer_;
|
|
return SendReliableDataFrame(
|
|
peer, encoded_ack.data(), encoded_ack.size(),
|
|
runtime->file_feedback_label_.c_str());
|
|
}
|
|
}
|
|
|
|
return SendReliableDataFrame(
|
|
runtime->peer_, encoded_ack.data(), encoded_ack.size(),
|
|
runtime->file_feedback_label_.c_str());
|
|
});
|
|
|
|
receiver.OnData(data, size);
|
|
return;
|
|
} else if (source_id == runtime->clipboard_label_) {
|
|
if (size > 0) {
|
|
std::string remote_user_id(user_id, user_id_size);
|
|
auto props =
|
|
runtime->FindRemoteSession(remote_user_id);
|
|
if (props && !props->clipboard_allowed_.load()) {
|
|
return;
|
|
}
|
|
|
|
runtime->clipboard_.QueueRemoteText(
|
|
data, size, props ? remote_user_id : std::string{});
|
|
}
|
|
return;
|
|
} else if (source_id == runtime->file_feedback_label_) {
|
|
runtime->transfers_.HandleAck(data, size);
|
|
return;
|
|
}
|
|
|
|
const std::string remote_id(user_id, user_id_size);
|
|
std::string json_str(data, size);
|
|
const auto preview_json =
|
|
json_str.find("\"deskhub_preview") != std::string::npos
|
|
? nlohmann::json::parse(json_str, nullptr, false)
|
|
: nlohmann::json{};
|
|
if (preview_json.is_object() &&
|
|
preview_json.value("type", nlohmann::json{}) == "deskhub_preview") {
|
|
if (source_id != runtime->control_data_label_ ||
|
|
!runtime->IsAuthorizedController(remote_id))
|
|
return;
|
|
const auto subscription = PreviewSubscription::Parse(preview_json);
|
|
if (!subscription) return;
|
|
const auto previous = runtime->preview_subscriptions_.Get(remote_id);
|
|
bool accepted = true;
|
|
// Apply encoder quality to this controller only; other controllers retain
|
|
// their own demand. MiniRTC owns per-peer video/audio pause state.
|
|
if (!previous || subscription->IsNewerThan(*previous)) {
|
|
if (subscription->fps > 0)
|
|
accepted = UpdateVideoSettings(
|
|
runtime->peer_, remote_id.data(), remote_id.size(),
|
|
static_cast<VideoQuality>(subscription->quality),
|
|
subscription->frame_rate,
|
|
static_cast<VideoDegradationPreference>(
|
|
subscription->EncoderPreference())) == 0;
|
|
if (accepted && (!previous || previous->max_bitrate_bps.value_or(0) !=
|
|
subscription->max_bitrate_bps.value_or(0)))
|
|
accepted = UpdateVideoBitrateLimit(
|
|
runtime->peer_, remote_id.data(), remote_id.size(),
|
|
subscription->max_bitrate_bps.value_or(0)) == 0;
|
|
// Rebalancing bandwidth alone must not force every device to send a
|
|
// keyframe. The pause API requests its own keyframes on resume.
|
|
if (accepted && (!previous || previous->width != subscription->width ||
|
|
previous->height != subscription->height))
|
|
accepted = UpdateVideoResolution(
|
|
runtime->peer_, remote_id.data(), remote_id.size(),
|
|
subscription->width, subscription->height) == 0;
|
|
if (accepted)
|
|
accepted = SetVideoSendPaused(runtime->peer_, remote_id.data(),
|
|
remote_id.size(), subscription->fps == 0) == 0;
|
|
if (accepted)
|
|
accepted = SetAudioSendPaused(runtime->peer_, remote_id.data(),
|
|
remote_id.size(), !subscription->audio) == 0;
|
|
if (accepted && subscription->audio)
|
|
runtime->devices_.StartSpeakerCapturer();
|
|
}
|
|
if (accepted) runtime->preview_subscriptions_.Set(remote_id, *subscription);
|
|
const auto reply = nlohmann::json{
|
|
{"type", "deskhub_preview_status"},
|
|
{"version", subscription->version},
|
|
{"request_id", subscription->request_id},
|
|
{"accepted",
|
|
accepted}}.dump();
|
|
SendReliableDataFrameToPeer(runtime->peer_, reply.data(), reply.size(),
|
|
runtime->control_data_label_.c_str(),
|
|
remote_id.data(), remote_id.size());
|
|
return;
|
|
}
|
|
if (preview_json.is_object() &&
|
|
preview_json.value("type", nlohmann::json{}) ==
|
|
"deskhub_preview_status") {
|
|
if (auto props = runtime->FindRemoteSession(remote_id);
|
|
props && source_id == props->control_data_label_) {
|
|
try {
|
|
if ((preview_json.at("version") == 1 ||
|
|
preview_json.at("version") == 2) &&
|
|
preview_json.at("request_id").is_number_unsigned() &&
|
|
preview_json.at("request_id").get<uint64_t>() > 0 &&
|
|
preview_json.at("request_id").get<uint64_t>() <= UINT32_MAX &&
|
|
preview_json.at("accepted").is_boolean()) {
|
|
props->preview_accepted_ = preview_json.at("accepted").get<bool>();
|
|
props->preview_ack_ = preview_json.at("request_id").get<uint32_t>();
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
RemoteAction remote_action{};
|
|
if (!remote_action.from_json(json_str)) {
|
|
LOG_ERROR("Failed to parse RemoteAction JSON payload");
|
|
return;
|
|
}
|
|
|
|
if (remote_action.type == ControlType::video_settings_status) {
|
|
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
|
if (source_id != props->control_data_label_) return;
|
|
std::lock_guard lock(props->video_settings_mutex_);
|
|
if (!props->video_settings_supported_) return;
|
|
if (remote_action.vs.accepted &&
|
|
static_cast<int32_t>(remote_action.vs.request_id -
|
|
props->applied_video_settings_.request_id) >= 0) {
|
|
props->applied_video_settings_ = remote_action.vs;
|
|
}
|
|
if (remote_action.vs.request_id != props->video_settings_.request_id) return;
|
|
props->video_settings_pending_ = false;
|
|
props->video_settings_failed_ = !remote_action.vs.accepted;
|
|
if (remote_action.vs.accepted) {
|
|
props->video_settings_ = remote_action.vs;
|
|
props->applied_video_settings_ = remote_action.vs;
|
|
} else {
|
|
const auto request_id = props->video_settings_.request_id;
|
|
props->video_settings_ = props->applied_video_settings_;
|
|
props->video_settings_.request_id = request_id;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (remote_action.type == ControlType::video_settings) {
|
|
if (source_id != runtime->control_data_label_ ||
|
|
!runtime->IsAuthorizedController(remote_id))
|
|
return;
|
|
const auto& settings = remote_action.vs;
|
|
remote_action.vs.accepted =
|
|
UpdateVideoSettings(
|
|
runtime->peer_, remote_id.data(), remote_id.size(),
|
|
static_cast<VideoQuality>(settings.quality), settings.frame_rate,
|
|
static_cast<VideoDegradationPreference>(settings.preference)) == 0;
|
|
remote_action.type = ControlType::video_settings_status;
|
|
const auto message = remote_action.to_json();
|
|
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(),
|
|
runtime->control_data_label_.c_str(),
|
|
remote_id.data(), remote_id.size());
|
|
return;
|
|
}
|
|
if (remote_action.type == ControlType::privacy_status) {
|
|
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
|
if (source_id != props->control_data_label_) return;
|
|
std::lock_guard lock(props->privacy_status_mutex_);
|
|
if (!props->privacy_status_received_ ||
|
|
static_cast<int32_t>(remote_action.ps.revision - props->privacy_status_.revision) >= 0) {
|
|
props->privacy_status_ = remote_action.ps;
|
|
props->privacy_status_received_ = true;
|
|
props->privacy_status_tick_ = SDL_GetTicks();
|
|
if (static_cast<int32_t>(remote_action.ps.revision - props->privacy_request_revision_) > 0 &&
|
|
remote_action.ps.state != PrivacyState::starting &&
|
|
remote_action.ps.state != PrivacyState::stopping)
|
|
props->privacy_command_pending_ = false;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (remote_action.type == ControlType::privacy_command) {
|
|
if (runtime->unattended_host_) return;
|
|
if (source_id == runtime->control_data_label_)
|
|
runtime->QueuePrivacyCommand(remote_id, remote_action.pc);
|
|
return;
|
|
}
|
|
const bool operates_host = remote_action.type == ControlType::mouse ||
|
|
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;
|
|
#ifdef _WIN32
|
|
// KeyboardController checks the desktop immediately before each native
|
|
// 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();
|
|
}
|
|
#endif
|
|
}
|
|
if (remote_action.type == ControlType::service_status) {
|
|
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
|
runtime->ApplyRemoteServiceStatus(*props, remote_action.ss);
|
|
}
|
|
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());
|
|
}
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
if (remote_action.type == ControlType::cursor_state) {
|
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
|
{
|
|
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
|
auto props_it = runtime->remote_sessions_.find(remote_id);
|
|
if (props_it != runtime->remote_sessions_.end()) {
|
|
props = props_it->second;
|
|
}
|
|
}
|
|
if (!props) {
|
|
return;
|
|
}
|
|
|
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
|
const uint32_t previous_seq = props->remote_cursor_state_.seq;
|
|
const bool is_newer =
|
|
!props->remote_cursor_state_received_ ||
|
|
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
|
|
if (is_newer) {
|
|
const bool changed =
|
|
!props->remote_cursor_state_received_ ||
|
|
props->remote_cursor_state_.visible != remote_action.cs.visible ||
|
|
props->remote_cursor_state_.shape != remote_action.cs.shape ||
|
|
props->remote_cursor_state_.render_mode !=
|
|
remote_action.cs.render_mode ||
|
|
props->remote_cursor_state_.hidden_reason !=
|
|
remote_action.cs.hidden_reason;
|
|
CursorState merged = remote_action.cs;
|
|
if (!remote_action.cs.position_update &&
|
|
props->remote_cursor_state_received_) {
|
|
merged.position_valid = props->remote_cursor_state_.position_valid;
|
|
merged.x = props->remote_cursor_state_.x;
|
|
merged.y = props->remote_cursor_state_.y;
|
|
merged.display_id = props->remote_cursor_state_.display_id;
|
|
}
|
|
props->remote_cursor_state_ = merged;
|
|
props->remote_cursor_state_received_ = true;
|
|
props->cursor_presentation_.Observe(merged, SDL_GetTicks());
|
|
if (changed) {
|
|
LOG_DEBUG("Received cursor state: seq={}, visible={}, shape={}, "
|
|
"mode={}, reason={}",
|
|
remote_action.cs.seq, remote_action.cs.visible,
|
|
static_cast<int>(remote_action.cs.shape),
|
|
static_cast<int>(remote_action.cs.render_mode),
|
|
static_cast<int>(remote_action.cs.hidden_reason));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (remote_action.type == ControlType::host_infomation) {
|
|
bool is_client_mode = false;
|
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
|
{
|
|
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
|
auto props_it = runtime->remote_sessions_.find(remote_id);
|
|
if (props_it != runtime->remote_sessions_.end()) {
|
|
is_client_mode = true;
|
|
props = props_it->second;
|
|
}
|
|
}
|
|
|
|
if (is_client_mode) {
|
|
if (props && source_id == props->control_data_label_) {
|
|
std::lock_guard lock(props->remote_version_mutex_);
|
|
props->remote_host_info_received_ = true;
|
|
props->remote_platform_ = remote_action.i.platform;
|
|
props->recent_platform_ = remote_action.i.platform;
|
|
props->remote_app_version_ = IsValidAppVersion(remote_action.i.app_version)
|
|
? remote_action.i.app_version : "";
|
|
}
|
|
// Negotiate live settings only on the reliable control stream. Old
|
|
// hosts omit this capability and retain their existing video behavior.
|
|
if (props && source_id == props->control_data_label_) {
|
|
std::lock_guard lock(props->video_settings_mutex_);
|
|
const bool first_advertisement = !props->video_settings_supported_;
|
|
props->preview_subscription_supported_ =
|
|
remote_action.i.supports_deskhub_preview;
|
|
props->preview_resolution_supported_ =
|
|
remote_action.i.supports_deskhub_resolution;
|
|
props->preview_fps15_supported_ = remote_action.i.supports_deskhub_fps15;
|
|
props->preview_bandwidth_supported_ = remote_action.i.supports_deskhub_bandwidth;
|
|
props->video_settings_supported_ =
|
|
remote_action.i.supports_video_settings;
|
|
if (first_advertisement && props->video_settings_supported_) {
|
|
RemoteAction settings{};
|
|
settings.type = ControlType::video_settings;
|
|
++props->video_settings_.request_id;
|
|
props->video_settings_pending_ = true;
|
|
props->video_settings_request_tick_ = SDL_GetTicks();
|
|
settings.vs = props->video_settings_;
|
|
const auto message = settings.to_json();
|
|
SendReliableDataFrame(props->peer_, message.data(), message.size(),
|
|
props->control_data_label_.c_str());
|
|
}
|
|
}
|
|
// client mode
|
|
if (props) {
|
|
std::lock_guard lock(props->remote_version_mutex_);
|
|
if (props->remote_host_name_.empty()) {
|
|
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
|
remote_action.i.host_name_size);
|
|
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
|
|
|
|
for (int i = 0; i < remote_action.i.display_num; i++) {
|
|
props->display_info_list_.push_back(
|
|
DisplayInfo(remote_action.i.display_list[i],
|
|
remote_action.i.left[i], remote_action.i.top[i],
|
|
remote_action.i.right[i], remote_action.i.bottom[i]));
|
|
}
|
|
}
|
|
props->recent_connection_dirty_ = true;
|
|
}
|
|
FreeRemoteAction(remote_action);
|
|
} else {
|
|
// server mode
|
|
std::string host_name(remote_action.i.host_name,
|
|
remote_action.i.host_name_size);
|
|
{
|
|
std::unique_lock lock(runtime->connection_status_mutex_);
|
|
runtime->connection_host_names_[remote_id] = host_name;
|
|
}
|
|
LOG_INFO("Remote hostname: [{}]", host_name);
|
|
if (source_id == runtime->control_data_label_) {
|
|
runtime->SetControllerPrivacySupport(
|
|
remote_id, remote_action.i.supports_privacy_screen);
|
|
}
|
|
FreeRemoteAction(remote_action);
|
|
}
|
|
} else {
|
|
// remote
|
|
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
|
|
// command arrives after it (e.g. when unmuting an inactive window).
|
|
const auto subscription = runtime->preview_subscriptions_.Get(remote_id);
|
|
const bool paused = subscription ? !subscription->audio : !remote_action.a;
|
|
const int ret = SetAudioSendPaused(runtime->peer_, remote_id.data(),
|
|
remote_id.size(), paused);
|
|
if (remote_action.a)
|
|
runtime->devices_.StartSpeakerCapturer();
|
|
else if (ret != 0)
|
|
// Legacy/web transports without the per-peer API retain their fallback.
|
|
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
|