diff --git a/scripts/windows/crossdesk.manifest b/scripts/windows/crossdesk.manifest
index 28530aa..a66a661 100644
--- a/scripts/windows/crossdesk.manifest
+++ b/scripts/windows/crossdesk.manifest
@@ -1,17 +1,13 @@
-
-
CrossDesk Application
-
@@ -20,24 +16,17 @@
-
-
- true/pm
-
- PerMonitorV2
+ true/pm
+ PerMonitorV2
-
-
-
-
-
\ No newline at end of file
+
diff --git a/scripts/windows/crossdesk.rc b/scripts/windows/crossdesk.rc
index a67ef6c..2783240 100644
--- a/scripts/windows/crossdesk.rc
+++ b/scripts/windows/crossdesk.rc
@@ -1,2 +1,11 @@
// Application icon resource; load by the resource name IDI_ICON1.
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
+
+#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
+#define RT_MANIFEST 24
+
+#ifdef CROSSDESK_DEBUG
+CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
+#else
+CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
+#endif
diff --git a/scripts/windows/crossdesk_debug.manifest b/scripts/windows/crossdesk_debug.manifest
new file mode 100644
index 0000000..9a7a610
--- /dev/null
+++ b/scripts/windows/crossdesk_debug.manifest
@@ -0,0 +1,32 @@
+
+
+
+
+
+ CrossDesk Application
+
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
+
diff --git a/src/device_controller/mouse/windows/mouse_controller.cpp b/src/device_controller/mouse/windows/mouse_controller.cpp
index 570976f..b066171 100644
--- a/src/device_controller/mouse/windows/mouse_controller.cpp
+++ b/src/device_controller/mouse/windows/mouse_controller.cpp
@@ -1,5 +1,7 @@
#include "mouse_controller.h"
+#include
+
#include "rd_log.h"
namespace crossdesk {
@@ -18,7 +20,14 @@ int MouseController::Destroy() { return 0; }
int MouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) {
- INPUT ip;
+ if (display_index < 0 ||
+ display_index >= static_cast(display_info_list_.size())) {
+ LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
+ display_index, display_info_list_.size());
+ return -1;
+ }
+
+ INPUT ip = {0};
if (remote_action.type == ControlType::mouse) {
ip.type = INPUT_MOUSE;
@@ -63,13 +72,25 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
ip.mi.time = 0;
- SetCursorPos(ip.mi.dx, ip.mi.dy);
+ if (!SetCursorPos(ip.mi.dx, ip.mi.dy)) {
+ LOG_WARN("SetCursorPos failed for mouse x={}, y={}, flag={}, err={}",
+ ip.mi.dx, ip.mi.dy, static_cast(remote_action.m.flag),
+ GetLastError());
+ return -1;
+ }
if (ip.mi.dwFlags != MOUSEEVENTF_MOVE) {
- SendInput(1, &ip, sizeof(INPUT));
+ UINT sent = SendInput(1, &ip, sizeof(INPUT));
+ if (sent != 1) {
+ LOG_WARN(
+ "SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
+ ip.mi.dx, ip.mi.dy, remote_action.m.s,
+ static_cast(remote_action.m.flag), GetLastError());
+ return -1;
+ }
}
}
return 0;
}
-} // namespace crossdesk
\ No newline at end of file
+} // namespace crossdesk
diff --git a/src/service/windows/service_host.cpp b/src/service/windows/service_host.cpp
index 1c8af77..fc18652 100644
--- a/src/service/windows/service_host.cpp
+++ b/src/service/windows/service_host.cpp
@@ -46,6 +46,13 @@ struct InputDesktopInfo {
std::string name;
};
+struct SecureDesktopMouseRequest {
+ int x = 0;
+ int y = 0;
+ int wheel = 0;
+ int flag = 0;
+};
+
struct ScopedEnvironmentBlock {
~ScopedEnvironmentBlock() {
if (environment != nullptr) {
@@ -339,6 +346,14 @@ std::string BuildSecureInputHelperKeyboardCommand(int key_code, bool is_down,
return stream.str();
}
+std::string BuildSecureInputHelperMouseCommand(int x, int y, int wheel,
+ int flag) {
+ std::ostringstream stream;
+ stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":"
+ << wheel << ":" << flag;
+ return stream.str();
+}
+
bool ParseSecureDesktopKeyboardIpcCommand(const std::string& command,
int* key_code_out, bool* is_down_out,
uint32_t* scan_code_out,
@@ -412,6 +427,57 @@ bool ParseSecureDesktopKeyboardIpcCommand(const std::string& command,
return false;
}
+bool ParseSecureDesktopMouseIpcCommand(const std::string& command,
+ SecureDesktopMouseRequest* request_out) {
+ if (request_out == nullptr) {
+ return false;
+ }
+
+ if (command.rfind(kSecureDesktopMouseIpcCommandPrefix, 0) != 0) {
+ return false;
+ }
+
+ const size_t x_begin = sizeof(kSecureDesktopMouseIpcCommandPrefix) - 1;
+ size_t separator = command.find(':', x_begin);
+ if (separator == std::string::npos) {
+ return false;
+ }
+
+ try {
+ request_out->x = std::stoi(command.substr(x_begin, separator - x_begin));
+ } catch (...) {
+ return false;
+ }
+
+ const size_t y_begin = separator + 1;
+ separator = command.find(':', y_begin);
+ if (separator == std::string::npos) {
+ return false;
+ }
+
+ try {
+ request_out->y = std::stoi(command.substr(y_begin, separator - y_begin));
+ } catch (...) {
+ return false;
+ }
+
+ const size_t wheel_begin = separator + 1;
+ separator = command.find(':', wheel_begin);
+ if (separator == std::string::npos) {
+ return false;
+ }
+
+ try {
+ request_out->wheel =
+ std::stoi(command.substr(wheel_begin, separator - wheel_begin));
+ request_out->flag = std::stoi(command.substr(separator + 1));
+ } catch (...) {
+ return false;
+ }
+
+ return true;
+}
+
bool CreateSessionSystemToken(DWORD session_id, HANDLE* token_out,
DWORD* error_code_out = nullptr) {
if (token_out == nullptr) {
@@ -1759,7 +1825,13 @@ std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command) {
if (ParseSecureDesktopKeyboardIpcCommand(normalized, &key_code, &is_down,
&scan_code, &extended)) {
return SendSecureDesktopKeyboardInput(key_code, is_down, scan_code,
- extended);
+ extended);
+ }
+ SecureDesktopMouseRequest mouse_request;
+ if (ParseSecureDesktopMouseIpcCommand(normalized, &mouse_request)) {
+ return SendSecureDesktopMouseInput(mouse_request.x, mouse_request.y,
+ mouse_request.wheel,
+ mouse_request.flag);
}
return BuildErrorJson("unknown_command");
}
@@ -2014,6 +2086,45 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
1000);
}
+std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
+ int wheel,
+ int flag) {
+ RefreshSessionState();
+ ReapSecureInputHelper();
+ EnsureSessionHelper();
+
+ DWORD target_session_id = 0xFFFFFFFF;
+ bool helper_running = false;
+ bool can_inject = false;
+ {
+ std::lock_guard lock(state_mutex_);
+ target_session_id = active_session_id_;
+ helper_running = secure_input_helper_running_ &&
+ secure_input_helper_session_id_ == target_session_id;
+ can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
+ }
+
+ if (target_session_id == 0xFFFFFFFF) {
+ return BuildErrorJson("no_active_console_session");
+ }
+ if (!can_inject) {
+ return BuildErrorJson("secure_input_not_active");
+ }
+
+ if (!helper_running) {
+ StopSecureInputHelper();
+ if (!LaunchSecureInputHelper(target_session_id)) {
+ std::lock_guard lock(state_mutex_);
+ return BuildErrorJson(secure_input_helper_last_error_.c_str(),
+ secure_input_helper_last_error_code_);
+ }
+ }
+
+ return QueryNamedPipeMessage(
+ GetCrossDeskSecureInputHelperPipeName(target_session_id),
+ BuildSecureInputHelperMouseCommand(x, y, wheel, flag), 1000);
+}
+
bool InstallCrossDeskService(const std::wstring& binary_path) {
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (manager == nullptr) {
@@ -2238,4 +2349,4 @@ std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms);
}
-} // namespace crossdesk
\ No newline at end of file
+} // namespace crossdesk
diff --git a/src/service/windows/service_host.h b/src/service/windows/service_host.h
index d0dba77..efa9baf 100644
--- a/src/service/windows/service_host.h
+++ b/src/service/windows/service_host.h
@@ -64,6 +64,7 @@ class CrossDeskServiceHost {
std::string SendSecureDesktopKeyboardInput(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false);
+ std::string SendSecureDesktopMouseInput(int x, int y, int wheel, int flag);
static void WINAPI ServiceMain(DWORD argc, LPWSTR* argv);
static BOOL WINAPI ConsoleControlHandler(DWORD control_type);
@@ -150,4 +151,4 @@ std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
} // namespace crossdesk
-#endif
\ No newline at end of file
+#endif
diff --git a/tests/windows_manifest_resource_test.cpp b/tests/windows_manifest_resource_test.cpp
new file mode 100644
index 0000000..f63e68b
--- /dev/null
+++ b/tests/windows_manifest_resource_test.cpp
@@ -0,0 +1,117 @@
+#include
+#include
+#include
+#include
+#include
+
+#ifdef _WIN32
+#include
+#endif
+
+namespace {
+
+std::string ReadFile(const std::filesystem::path& path) {
+ std::ifstream file(path, std::ios::binary);
+ if (!file) {
+ return {};
+ }
+
+ std::ostringstream stream;
+ stream << file.rdbuf();
+ return stream.str();
+}
+
+std::filesystem::path FindRepoRoot() {
+ std::filesystem::path current = std::filesystem::current_path();
+ while (!current.empty()) {
+ if (std::filesystem::exists(current / "xmake.lua") &&
+ std::filesystem::exists(current / "scripts/windows/crossdesk.rc")) {
+ return current;
+ }
+ current = current.parent_path();
+ }
+ return {};
+}
+
+bool ExpectContains(const char* name, const std::string& value,
+ const std::string& expected) {
+ if (value.find(expected) != std::string::npos) {
+ return true;
+ }
+
+ std::cerr << name << " missing expected text: " << expected << "\n";
+ return false;
+}
+
+bool ExpectNotContains(const char* name, const std::string& value,
+ const std::string& unexpected) {
+ if (value.find(unexpected) == std::string::npos) {
+ return true;
+ }
+
+ std::cerr << name << " contains unexpected text: " << unexpected << "\n";
+ return false;
+}
+
+#ifdef _WIN32
+bool ExpectActivationContext(const std::filesystem::path& manifest_path) {
+ ACTCTXW context = {};
+ context.cbSize = sizeof(context);
+ std::wstring source = manifest_path.wstring();
+ context.lpSource = source.c_str();
+
+ HANDLE activation_context = CreateActCtxW(&context);
+ if (activation_context == INVALID_HANDLE_VALUE) {
+ std::cerr << "CreateActCtxW failed for " << manifest_path.string()
+ << ", error=" << GetLastError() << "\n";
+ return false;
+ }
+
+ ReleaseActCtx(activation_context);
+ return true;
+}
+#endif
+
+} // namespace
+
+int main() {
+ const std::filesystem::path repo_root = FindRepoRoot();
+ if (repo_root.empty()) {
+ std::cerr << "failed to locate repository root\n";
+ return 1;
+ }
+
+ const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc");
+ const std::string manifest =
+ ReadFile(repo_root / "scripts/windows/crossdesk.manifest");
+ const std::string debug_manifest =
+ ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest");
+
+ bool ok = true;
+ ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest");
+ ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest");
+ ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG");
+ ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST");
+ ok &= ExpectContains("crossdesk.manifest", manifest,
+ "level=\"requireAdministrator\"");
+ ok &= ExpectContains("crossdesk.manifest", manifest,
+ "http://schemas.microsoft.com/SMI/2005/WindowsSettings");
+ ok &= ExpectContains("crossdesk.manifest", manifest,
+ "http://schemas.microsoft.com/SMI/2016/WindowsSettings");
+ ok &= ExpectNotContains("crossdesk.manifest", manifest,
+ "processorArchitecture=\"*\"");
+ ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
+ "level=\"asInvoker\"");
+ ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
+ "http://schemas.microsoft.com/SMI/2005/WindowsSettings");
+ ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
+ "http://schemas.microsoft.com/SMI/2016/WindowsSettings");
+ ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest,
+ "processorArchitecture=\"*\"");
+#ifdef _WIN32
+ ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
+ ok &= ExpectActivationContext(
+ repo_root / "scripts/windows/crossdesk_debug.manifest");
+#endif
+ return ok ? 0 : 1;
+}
diff --git a/tests/windows_mouse_controller_safety_test.cpp b/tests/windows_mouse_controller_safety_test.cpp
new file mode 100644
index 0000000..6a9e9f5
--- /dev/null
+++ b/tests/windows_mouse_controller_safety_test.cpp
@@ -0,0 +1,63 @@
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+std::filesystem::path FindRepoRoot() {
+ std::filesystem::path current = std::filesystem::current_path();
+ while (!current.empty()) {
+ if (std::filesystem::exists(current / "xmake.lua") &&
+ std::filesystem::exists(
+ current / "src/device_controller/mouse/windows/mouse_controller.cpp")) {
+ return current;
+ }
+ current = current.parent_path();
+ }
+ return {};
+}
+
+std::string ReadFile(const std::filesystem::path& path) {
+ std::ifstream file(path, std::ios::binary);
+ if (!file) {
+ return {};
+ }
+
+ std::ostringstream stream;
+ stream << file.rdbuf();
+ return stream.str();
+}
+
+bool ExpectContains(const char* name, const std::string& value,
+ const std::string& expected) {
+ if (value.find(expected) != std::string::npos) {
+ return true;
+ }
+
+ std::cerr << name << " missing expected text: " << expected << "\n";
+ return false;
+}
+
+} // namespace
+
+int main() {
+ const std::filesystem::path repo_root = FindRepoRoot();
+ if (repo_root.empty()) {
+ std::cerr << "failed to locate repository root\n";
+ return 1;
+ }
+
+ const std::string mouse_controller = ReadFile(
+ repo_root / "src/device_controller/mouse/windows/mouse_controller.cpp");
+
+ bool ok = true;
+ ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
+ "INPUT ip = {0};");
+ ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
+ "SetCursorPos failed");
+ ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
+ "SendInput failed for mouse");
+ return ok ? 0 : 1;
+}
diff --git a/tests/windows_service_mouse_ipc_test.cpp b/tests/windows_service_mouse_ipc_test.cpp
new file mode 100644
index 0000000..52e6333
--- /dev/null
+++ b/tests/windows_service_mouse_ipc_test.cpp
@@ -0,0 +1,62 @@
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+
+std::filesystem::path FindRepoRoot() {
+ std::filesystem::path current = std::filesystem::current_path();
+ while (!current.empty()) {
+ if (std::filesystem::exists(current / "xmake.lua") &&
+ std::filesystem::exists(current / "src/service/windows/service_host.cpp")) {
+ return current;
+ }
+ current = current.parent_path();
+ }
+ return {};
+}
+
+std::string ReadFile(const std::filesystem::path& path) {
+ std::ifstream file(path, std::ios::binary);
+ if (!file) {
+ return {};
+ }
+
+ std::ostringstream stream;
+ stream << file.rdbuf();
+ return stream.str();
+}
+
+bool ExpectContains(const char* name, const std::string& value,
+ const std::string& expected) {
+ if (value.find(expected) != std::string::npos) {
+ return true;
+ }
+
+ std::cerr << name << " missing expected text: " << expected << "\n";
+ return false;
+}
+
+} // namespace
+
+int main() {
+ const std::filesystem::path repo_root = FindRepoRoot();
+ if (repo_root.empty()) {
+ std::cerr << "failed to locate repository root\n";
+ return 1;
+ }
+
+ const std::string service_host =
+ ReadFile(repo_root / "src/service/windows/service_host.cpp");
+
+ bool ok = true;
+ ok &= ExpectContains("service_host.cpp", service_host,
+ "ParseSecureDesktopMouseIpcCommand");
+ ok &= ExpectContains("service_host.cpp", service_host,
+ "BuildSecureInputHelperMouseCommand");
+ ok &= ExpectContains("service_host.cpp", service_host,
+ "return SendSecureDesktopMouseInput");
+ return ok ? 0 : 1;
+}
diff --git a/xmake/targets.lua b/xmake/targets.lua
index d30d687..5cdd9af 100644
--- a/xmake/targets.lua
+++ b/xmake/targets.lua
@@ -39,6 +39,21 @@ function setup_targets()
add_includedirs("src/device_controller")
add_files("tests/macos_keyboard_modifier_state_test.cpp")
+ target("windows_manifest_resource_test")
+ set_kind("binary")
+ set_default(false)
+ add_files("tests/windows_manifest_resource_test.cpp")
+
+ target("windows_service_mouse_ipc_test")
+ set_kind("binary")
+ set_default(false)
+ add_files("tests/windows_service_mouse_ipc_test.cpp")
+
+ target("windows_mouse_controller_safety_test")
+ set_kind("binary")
+ set_default(false)
+ add_files("tests/windows_mouse_controller_safety_test.cpp")
+
target("screen_capturer")
set_kind("object")
add_deps("rd_log", "common")