3 Commits

Author SHA1 Message Date
yuanyuanxiang
f85cc8b86c Fix: Linux client UTF-8 path/active-window garbled on server 2026-05-08 14:03:45 +02:00
yuanyuanxiang
bc06fd5af5 Feature: Linux/macOS server-identity gate via libsign.a
fix remote-cursor flicker on Windows controller
2026-05-08 12:39:59 +02:00
yuanyuanxiang
731ff7a894 Feature: right-click region screenshot in non-control mode 2026-05-08 09:27:19 +02:00
15 changed files with 414 additions and 60 deletions

11
.gitignore vendored
View File

@@ -74,3 +74,14 @@ test/build/
docs/MultiLayerLicense_Design.md docs/MultiLayerLicense_Design.md
docs/MultiLayerLicense_Implementation.md docs/MultiLayerLicense_Implementation.md
docs/_CodeReference.md docs/_CodeReference.md
linux/CMakeFiles/*
Releases/*
*.log
*.txt
linux/Makefile
linux/cmake_install.cmake
.vs
docs/macOS_Support_Design.md
settings.local.json
*.zip
*.lic

52
client/sign_shim_unix.cpp Normal file
View File

@@ -0,0 +1,52 @@
// sign_shim_unix.cpp - Linux/macOS adapter for libsign.a's C interface
//
// libsign.a 公开 ABI 是 C linkage避免 std::string 跨编译器/跨 libstdc++
// 版本 ABI 风险),但 YAMA 客户端代码IOCPClient.cpp / KernelManager.cpp /
// linux/main.cpp / macos/main.mm习惯用 std::string 调用 signMessage /
// verifyMessage。本文件提供 C++ 适配,让两边契合。
//
// Windows 不编译这个文件——Windows 直接链接私有 .lib 提供的 std::string 版本。
#include <string>
#include <cstring>
// libsign.a 提供的 C 接口
extern "C" {
int signMessage_c(const char* privateKey, int privateKeyLen,
const unsigned char* msg, int msgLen,
char* outBuf, int outBufSize);
int verifyMessage_c(const char* publicKey, int publicKeyLen,
const unsigned char* msg, int msgLen,
const char* sigHex, int sigLen);
int isVerifyCalled_c(void);
}
// 与 YAMA common/commands.h 中 BYTE 一致
typedef unsigned char BYTE;
// ============================================================================
// 提供 YAMA 既有声明所期望的 C++ 符号
// ============================================================================
std::string signMessage(const std::string& privateKey, BYTE* msg, int len)
{
char buf[65] = {};
int n = signMessage_c(privateKey.c_str(), (int)privateKey.size(),
msg, len,
buf, sizeof(buf));
if (n != 64) return std::string();
return std::string(buf, 64);
}
bool verifyMessage(const std::string& publicKey, BYTE* msg, int len,
const std::string& signature)
{
return verifyMessage_c(publicKey.c_str(), (int)publicKey.size(),
msg, len,
signature.data(), (int)signature.size()) != 0;
}
int isVerifyCalled()
{
return isVerifyCalled_c();
}

View File

@@ -1014,7 +1014,14 @@ typedef struct LOGIN_INFOR {
{ {
memset(this, 0, sizeof(LOGIN_INFOR)); memset(this, 0, sizeof(LOGIN_INFOR));
bToken = TOKEN_LOGIN; bToken = TOKEN_LOGIN;
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, CLIENT_CAP_V2 | CLIENT_CAP_UTF8 | CLIENT_CAP_SCREEN_PREVIEW); // 能力位声明客户端实际实现了的功能。SCREEN_PREVIEW 只在 Windows 客户端
// 实现(依赖 GDI BitBlt + GDI+ JPEGLinux/macOS 不声明,避免服务端发请求
// 后等 4s 超时显示"预览不可用"。
unsigned int caps = CLIENT_CAP_V2 | CLIENT_CAP_UTF8;
#ifdef _WIN32
caps |= CLIENT_CAP_SCREEN_PREVIEW;
#endif
sprintf_s(moduleVersion, "%s-%04X", DLL_VERSION, caps);
} }
LOGIN_INFOR& Speed(unsigned long speed) LOGIN_INFOR& Speed(unsigned long speed)
{ {

View File

@@ -28,6 +28,7 @@ set(SOURCES
main.cpp main.cpp
../client/Buffer.cpp ../client/Buffer.cpp
../client/IOCPClient.cpp ../client/IOCPClient.cpp
../client/sign_shim_unix.cpp
) )
add_executable(ghost ${SOURCES}) add_executable(ghost ${SOURCES})
@@ -40,6 +41,14 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a") message(STATUS "链接库文件: ${CMAKE_SOURCE_DIR}/lib/libzstd.a")
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libzstd.a") target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libzstd.a")
# 链接私有签名库(提供 signMessage / verifyMessage源码不开源
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
target_link_libraries(ghost PRIVATE "${CMAKE_SOURCE_DIR}/lib/libsign.a")
# libsign.a 内部使用 OpenSSL HMAC需要在最终可执行链接 libcrypto
find_package(OpenSSL REQUIRED)
target_link_libraries(ghost PRIVATE OpenSSL::Crypto)
# 链接 dl 库dlopen/dlsym 用于运行时加载 X11 # 链接 dl 库dlopen/dlsym 用于运行时加载 X11
target_link_libraries(ghost PRIVATE dl) target_link_libraries(ghost PRIVATE dl)

BIN
linux/lib/libsign.a Normal file

Binary file not shown.

View File

@@ -46,6 +46,11 @@ static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重
// 客户端 IDV2 文件传输需要) // 客户端 IDV2 文件传输需要)
uint64_t g_myClientID = 0; uint64_t g_myClientID = 0;
// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验
std::string g_loginMsg;
time_t g_loginTime = 0;
bool g_settingsVerified = false;
// ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ============== // ============== UTF-8 → GBK 编码转换(服务端为 Windows GBK 环境) ==============
static std::string utf8ToGbk(const std::string& utf8) static std::string utf8ToGbk(const std::string& utf8)
@@ -467,18 +472,38 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
uint64_t now = GetUnixMs(); uint64_t now = GetUnixMs();
double rtt_ms = (double)(now - ack->Time); double rtt_ms = (double)(now - ack->Time);
g_rttEstimator.update_from_sample(rtt_ms); g_rttEstimator.update_from_sample(rtt_ms);
// 心跳节奏太密日志会刷屏;最多 60s 一行
static time_t lastAckLog = 0;
time_t now_s = time(nullptr);
if (now_s - lastAckLog >= 60) {
lastAckLog = now_s;
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n", Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
user, rtt_ms, g_rttEstimator.srtt * 1000); user, rtt_ms, g_rttEstimator.srtt * 1000);
} }
}
} else if (szBuffer[0] == CMD_MASTERSETTING) { } else if (szBuffer[0] == CMD_MASTERSETTING) {
int settingSize = ulLength - 1; int settingSize = ulLength - 1;
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval // 强制要求完整 MasterSettings包含 Signature 字段)。包不完整 → 视为非授权服务端
if (settingSize < (int)sizeof(MasterSettings)) {
g_bExit = S_CLIENT_EXIT;
return TRUE;
}
MasterSettings settings = {}; MasterSettings settings = {};
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings)); memcpy(&settings, szBuffer + 1, sizeof(MasterSettings));
// 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature
// 验证签名。失败 → 静默退出(不打印关键词日志)
extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
std::string sig((char*)settings.Signature, (char*)settings.Signature + sizeof(settings.Signature));
if (!verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) {
g_bExit = S_CLIENT_EXIT;
return TRUE;
}
g_settingsVerified = true;
if (settings.ReportInterval > 0) if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval; g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
}
} else if (szBuffer[0] == COMMAND_NEXT) { } else if (szBuffer[0] == COMMAND_NEXT) {
Mprintf("** [%p] Received 'NEXT' command ***\n", user); Mprintf("** [%p] Received 'NEXT' command ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_TEXT) { } else if (szBuffer[0] == COMMAND_C2C_TEXT) {
@@ -1090,6 +1115,9 @@ int main(int argc, char* argv[])
logInfo.AddReserved((int)getpid()); // [17] RES_PID logInfo.AddReserved((int)getpid()); // [17] RES_PID
logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE logInfo.AddReserved(getFileSize(exePath).c_str()); // [18] RES_FILESIZE
// 服务端签名输入:与服务端 AddList 处签名格式一致startTime + "|" + clientID
g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID);
// 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段) // 初始化用户活动检测器(用于心跳包中的 ActiveWnd 字段)
ActivityChecker activityChecker; ActivityChecker activityChecker;
@@ -1102,6 +1130,9 @@ int main(int argc, char* argv[])
continue; continue;
} }
// 进入新连接,重置服务端身份校验状态
g_loginTime = time(nullptr);
g_settingsVerified = false;
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c)); ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT // 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
@@ -1131,8 +1162,19 @@ int main(int argc, char* argv[])
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL) if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break; break;
// 兜底:登录后 30 秒内必须收到并通过 MasterSettings 校验,否则视为非授权服务端
if (!g_settingsVerified && g_loginTime > 0 &&
time(nullptr) - g_loginTime > 30) {
g_bExit = S_CLIENT_EXIT;
break;
}
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致) // 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
std::string activity = utf8ToGbk(activityChecker.Check()); // ActiveWnd 直接发 UTF-8——与 LOGIN_INFOR.moduleVersion 中声明的
// CLIENT_CAP_UTF8 一致;服务端按 cap 位用 CP_UTF8 解码。早期为兼容
// MBCS 老服务端做过 utf8ToGbk 转换,但现在新版 Linux 客户端经
// libsign 网关只能连新版服务端,无需再转。
std::string activity = activityChecker.Check();
Heartbeat hb; Heartbeat hb;
hb.Time = GetUnixMs(); hb.Time = GetUnixMs();
@@ -1143,10 +1185,16 @@ int main(int argc, char* argv[])
buf[0] = TOKEN_HEARTBEAT; buf[0] = TOKEN_HEARTBEAT;
memcpy(buf + 1, &hb, sizeof(Heartbeat)); memcpy(buf + 1, &hb, sizeof(Heartbeat));
ClientObject->Send2Server((char*)buf, sizeof(buf)); ClientObject->Send2Server((char*)buf, sizeof(buf));
// 心跳节奏太密日志会刷屏;最多 60s 一行
static time_t lastSendLog = 0;
time_t now_s = time(nullptr);
if (now_s - lastSendLog >= 60) {
lastSendLog = now_s;
Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n", Mprintf(">>> Heartbeat sent: Ping=%dms, Interval=%ds, Activity=%s\n",
hb.Ping, interval, activity.c_str()); hb.Ping, interval, activity.c_str());
} }
} }
}
Logger::getInstance().stop(); Logger::getInstance().stop();
removePidFile(); removePidFile();

View File

@@ -19,6 +19,7 @@ set(SOURCES
main.mm main.mm
../client/Buffer.cpp ../client/Buffer.cpp
../client/IOCPClient.cpp ../client/IOCPClient.cpp
../client/sign_shim_unix.cpp
ScreenHandler.mm ScreenHandler.mm
InputHandler.mm InputHandler.mm
SystemManager.mm SystemManager.mm
@@ -62,6 +63,11 @@ target_link_libraries(ghost PRIVATE
${ACCELERATE_FRAMEWORK} ${ACCELERATE_FRAMEWORK}
${ICONV_LIBRARY} ${ICONV_LIBRARY}
"${CMAKE_SOURCE_DIR}/lib/libzstd.a" "${CMAKE_SOURCE_DIR}/lib/libzstd.a"
# 私有签名库(提供 signMessage / verifyMessage源码不开源
# 该库由 SimplePlugins 仓库本地构建后放置于 lib/,构建机需先准备好
# libsign.a 内部使用 macOS CommonCryptoHMAC-SHA256CCHmac 在 libSystem
# 中已被 Cocoa/CoreFoundation 等链接自动引入,故此处无需额外 framework
"${CMAKE_SOURCE_DIR}/lib/libsign.a"
) )
# Compiler flags # Compiler flags

BIN
macos/lib/libsign.a Normal file

Binary file not shown.

View File

@@ -36,6 +36,11 @@ static std::atomic<bool> g_needResendLogin(false); // 分组变更后需要重
// Client ID (calculated from system info, used by ScreenHandler) // Client ID (calculated from system info, used by ScreenHandler)
uint64_t g_myClientID = 0; uint64_t g_myClientID = 0;
// 服务端身份校验:登录消息(签名输入),登录时间,是否已通过校验
std::string g_loginMsg;
time_t g_loginTime = 0;
bool g_settingsVerified = false;
// 远程地址:当前为写死状态,如需调试,请按实际情况修改 // 远程地址:当前为写死状态,如需调试,请按实际情况修改
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS }; CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
@@ -626,6 +631,9 @@ static void fillLoginInfo(LOGIN_INFOR& info)
} }
info.AddReserved(std::to_string(g_myClientID).c_str()); info.AddReserved(std::to_string(g_myClientID).c_str());
// 服务端签名输入:与服务端 AddList 处签名格式一致startTime + "|" + clientID
g_loginMsg = std::string(info.szStartTime) + "|" + std::to_string(g_myClientID);
NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu", NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID); osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
} }
@@ -849,13 +857,27 @@ int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
} }
} else if (szBuffer[0] == CMD_MASTERSETTING) { } else if (szBuffer[0] == CMD_MASTERSETTING) {
int settingSize = ulLength - 1; int settingSize = ulLength - 1;
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval // 强制要求完整 MasterSettings包含 Signature 字段)。包不完整 → 视为非授权服务端
if (settingSize < (int)sizeof(MasterSettings)) {
g_bExit = S_CLIENT_EXIT;
return TRUE;
}
MasterSettings settings = {}; MasterSettings settings = {};
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings)); memcpy(&settings, szBuffer + 1, sizeof(MasterSettings));
// 服务端身份校验:用 g_loginMsg (= szStartTime + "|" + clientID) 与 settings.Signature
// 验证签名。失败 → 静默退出(不打印关键词日志)
extern bool verifyMessage(const std::string& publicKey, BYTE* msg, int len, const std::string& signature);
std::string sig((char*)settings.Signature, (char*)settings.Signature + sizeof(settings.Signature));
if (!verifyMessage("", (BYTE*)g_loginMsg.data(), (int)g_loginMsg.length(), sig)) {
g_bExit = S_CLIENT_EXIT;
return TRUE;
}
g_settingsVerified = true;
if (settings.ReportInterval > 0) if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval; g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval); Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
}
} else if (szBuffer[0] == COMMAND_NEXT) { } else if (szBuffer[0] == COMMAND_NEXT) {
Mprintf("** [%p] Received 'NEXT' command ***\n", user); Mprintf("** [%p] Received 'NEXT' command ***\n", user);
} else if (szBuffer[0] == CMD_SET_GROUP) { } else if (szBuffer[0] == CMD_SET_GROUP) {
@@ -981,6 +1003,9 @@ int main(int argc, const char* argv[])
continue; continue;
} }
// 进入新连接,重置服务端身份校验状态
g_loginTime = time(nullptr);
g_settingsVerified = false;
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c)); ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT // 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
@@ -1002,6 +1027,13 @@ int main(int argc, const char* argv[])
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL) if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break; break;
// 兜底:登录后 30 秒内必须收到并通过 MasterSettings 校验,否则视为非授权服务端
if (!g_settingsVerified && g_loginTime > 0 &&
time(nullptr) - g_loginTime > 30) {
g_bExit = S_CLIENT_EXIT;
break;
}
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致) // 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
std::string activity = getActiveApp(); std::string activity = getActiveApp();

View File

@@ -178,15 +178,25 @@ bool SupportsFileTransferV2(context* ctx) {
} }
// 获取客户端协议字符串编码优先看自身能力位若是子连接CAPABILITIES 为空) // 获取客户端协议字符串编码优先看自身能力位若是子连接CAPABILITIES 为空)
// 则通过 peer IP 查主连接。找不到则默认 CP936。 // 则通过 peer IP 查主连接。Linux/macOS 客户端文件系统路径与 locale 现代发行版
// 默认就是 UTF-8——即便客户端二进制是早于 CLIENT_CAP_UTF8 引入commit 0aa7588
// 之前编译的,没声明 cap 位,事实上仍发 UTF-8 字节,按 client type 兜底走 UTF-8。
// 找不到则默认 CP936。
UINT GetClientEncoding(context* ctx) { UINT GetClientEncoding(context* ctx) {
if (!ctx) return 936; if (!ctx) return 936;
// 主连接情形CAPABILITIES 已由 LOGIN_INFOR 处理流程填好 // 主连接情形CAPABILITIES 已由 LOGIN_INFOR 处理流程填好
if (ctx->SupportsUtf8()) return CP_UTF8; if (ctx->SupportsUtf8()) return CP_UTF8;
// 客户端类型兜底LNX / MAC 默认 UTF-8兼容老二进制无 UTF-8 cap 位的情形)
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
if (clientType == "LNX" || clientType == "MAC") return CP_UTF8;
// 子连接情形CAPABILITIES 为空 -> 通过 IP 找主连接 // 子连接情形CAPABILITIES 为空 -> 通过 IP 找主连接
if (g_2015RemoteDlg) { if (g_2015RemoteDlg) {
context* mainCtx = g_2015RemoteDlg->FindHostByIP(ctx->GetPeerName()); context* mainCtx = g_2015RemoteDlg->FindHostByIP(ctx->GetPeerName());
if (mainCtx && mainCtx->SupportsUtf8()) return CP_UTF8; if (mainCtx) {
if (mainCtx->SupportsUtf8()) return CP_UTF8;
CString mainType = mainCtx->GetAdditionalData(RES_CLIENT_TYPE);
if (mainType == "LNX" || mainType == "MAC") return CP_UTF8;
}
} }
return 936; return 936;
} }
@@ -7550,6 +7560,28 @@ void CMy2015RemoteDlg::OnListClick(NMHDR* pNMHDR, LRESULT* pResult)
CString res[RES_MAX]; CString res[RES_MAX];
CString startTime = ctx->GetClientData(ONLINELIST_STARTTIME); CString startTime = ctx->GetClientData(ONLINELIST_STARTTIME);
ctx->GetAdditionalData(res); ctx->GetAdditionalData(res);
// 客户端 RES_* 字符串编码取决于客户端能力位UTF-8 客户端Linux/macOS/新 Win
// 发的是 UTF-8 字节,老客户端是 CP_ACP。这里统一规整到 CP_ACP让下游 FormatL
// 与既有 ANSI 字符串拼接以及最终 CP_ACP→wide 的浮窗渲染都能正确识别。
// 若服务端运行系统的 ANSI 代码页不能容纳客户端字符(如德语服务端遇到中文路径),
// 不可表示的字符会变 '?' —— 与项目其它路径的既有限制一致,不在本次修复范围。
UINT cp = GetClientEncoding(ctx);
if (cp != CP_ACP) {
for (int i = 0; i < RES_MAX; i++) {
if (res[i].IsEmpty()) continue;
int wlen = MultiByteToWideChar(cp, 0, res[i].GetString(), -1, NULL, 0);
if (wlen <= 1) continue;
std::wstring wbuf(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, res[i].GetString(), -1, &wbuf[0], wlen);
int alen = WideCharToMultiByte(CP_ACP, 0, wbuf.c_str(), -1, NULL, 0, NULL, NULL);
if (alen <= 1) continue;
CString out;
WideCharToMultiByte(CP_ACP, 0, wbuf.c_str(), -1,
out.GetBufferSetLength(alen - 1), alen, NULL, NULL);
out.ReleaseBuffer(alen - 1);
res[i] = out;
}
}
FlagType type = ctx->GetFlagType(); FlagType type = ctx->GetFlagType();
static std::map<FlagType, std::string> typMap = { static std::map<FlagType, std::string> typMap = {
{FLAG_WINOS, "WinOS"}, {FLAG_UNKNOWN, "Unknown"}, {FLAG_SHINE, "Shine"}, {FLAG_WINOS, "WinOS"}, {FLAG_UNKNOWN, "Unknown"}, {FLAG_SHINE, "Shine"},

View File

@@ -57,9 +57,6 @@ public:
} else { } else {
m_langDir = langDir; m_langDir = langDir;
} }
// 确保目录存在
CreateDirectory(m_langDir, NULL);
} }
// 获取可用的语言列表(包括内嵌语言) // 获取可用的语言列表(包括内嵌语言)

View File

@@ -489,6 +489,8 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
ON_WM_VSCROLL() ON_WM_VSCROLL()
ON_WM_LBUTTONDOWN() ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP() ON_WM_LBUTTONUP()
ON_WM_RBUTTONDOWN()
ON_WM_RBUTTONUP()
ON_WM_MOUSEWHEEL() ON_WM_MOUSEWHEEL()
ON_WM_MOUSEMOVE() ON_WM_MOUSEMOVE()
ON_WM_MOUSELEAVE() ON_WM_MOUSELEAVE()
@@ -497,6 +499,7 @@ BEGIN_MESSAGE_MAP(CScreenSpyDlg, CDialog)
ON_WM_LBUTTONDBLCLK() ON_WM_LBUTTONDBLCLK()
ON_WM_ACTIVATE() ON_WM_ACTIVATE()
ON_WM_TIMER() ON_WM_TIMER()
ON_WM_ERASEBKGND()
ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen) ON_COMMAND(ID_EXIT_FULLSCREEN, &CScreenSpyDlg::OnExitFullscreen)
ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo) ON_COMMAND(ID_SHOW_STATUS_INFO, &CScreenSpyDlg::OnShowStatusInfo)
ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo) ON_COMMAND(ID_HIDE_STATUS_INFO, &CScreenSpyDlg::OnHideStatusInfo)
@@ -689,7 +692,7 @@ BOOL CScreenSpyDlg::OnInitDialog()
if (m_bIsCtrl) { if (m_bIsCtrl) {
ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME ImmAssociateContext(m_hWnd, NULL); // 控制模式:禁用 IME
} }
m_bIsTraceCursor = FALSE; //不是跟踪 m_bIsTraceCursor = !m_bIsCtrl; // 非控制状态,则跟踪鼠标
m_ClientCursorPos.x = 0; m_ClientCursorPos.x = 0;
m_ClientCursorPos.y = 0; m_ClientCursorPos.y = 0;
m_bCursorIndex = 0; m_bCursorIndex = 0;
@@ -1606,6 +1609,19 @@ bool CScreenSpyDlg::Decode(LPBYTE Buffer, int size)
return false; return false;
} }
// 跳过默认背景擦除:随帧重绘时若先 FillRect 灰色再 BitBlt 帧,会在两步之间
// 出现"瞬时灰背景",启用远程光标(应用层 DrawIconEx)时尤其明显——光标随每帧重绘,
// 灰一闪 → 帧覆盖 → 重画光标,循环看上去就是光标频繁闪烁。
// adaptive/zoom 模式下 BitBlt/StretchBlt 覆盖整个客户区,本就不需要先擦;
// m_bIsFirst首帧未到达仍走默认擦除以避免显示残留内容。
BOOL CScreenSpyDlg::OnEraseBkgnd(CDC* pDC)
{
if (m_bIsFirst) {
return __super::OnEraseBkgnd(pDC);
}
return TRUE;
}
void CScreenSpyDlg::OnPaint() void CScreenSpyDlg::OnPaint()
{ {
if (m_bIsClosed) return; if (m_bIsClosed) return;
@@ -1641,16 +1657,19 @@ void CScreenSpyDlg::OnPaint()
BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY); BitBlt(m_hFullDC, 0, 0, srcW, srcH, m_hFullMemDC, m_ulHScrollPos, m_ulVScrollPos, SRCCOPY);
} }
// 绘制框选矩形 // 绘制框选矩形(左键放大用红色,右键截图用绿色,二者颜色错开避免误操作)
if (m_bSelectingZoom) { if (m_bSelectingZoom || m_bSelectingShot) {
CRect rcSelect; CPoint ptStart = m_bSelectingZoom ? m_ptZoomStart : m_ptShotStart;
rcSelect.left = min(m_ptZoomStart.x, m_ptZoomCurrent.x); CPoint ptCur = m_bSelectingZoom ? m_ptZoomCurrent : m_ptShotCurrent;
rcSelect.top = min(m_ptZoomStart.y, m_ptZoomCurrent.y); COLORREF clr = m_bSelectingZoom ? RGB(255, 0, 0) : RGB(0, 180, 0);
rcSelect.right = max(m_ptZoomStart.x, m_ptZoomCurrent.x);
rcSelect.bottom = max(m_ptZoomStart.y, m_ptZoomCurrent.y);
// 使用虚线边框绘制选择框 CRect rcSelect;
HPEN hPen = CreatePen(PS_DASH, 1, RGB(255, 0, 0)); rcSelect.left = min(ptStart.x, ptCur.x);
rcSelect.top = min(ptStart.y, ptCur.y);
rcSelect.right = max(ptStart.x, ptCur.x);
rcSelect.bottom = max(ptStart.y, ptCur.y);
HPEN hPen = CreatePen(PS_DASH, 1, clr);
HPEN hOldPen = (HPEN)SelectObject(m_hFullDC, hPen); HPEN hOldPen = (HPEN)SelectObject(m_hFullDC, hPen);
HBRUSH hOldBrush = (HBRUSH)SelectObject(m_hFullDC, GetStockObject(NULL_BRUSH)); HBRUSH hOldBrush = (HBRUSH)SelectObject(m_hFullDC, GetStockObject(NULL_BRUSH));
Rectangle(m_hFullDC, rcSelect.left, rcSelect.top, rcSelect.right, rcSelect.bottom); Rectangle(m_hFullDC, rcSelect.left, rcSelect.top, rcSelect.right, rcSelect.bottom);
@@ -2849,29 +2868,10 @@ void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
} }
// 将屏幕坐标转换为原图坐标 // 将屏幕坐标转换为原图坐标
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth; if (!ScreenRectToImageRect(rcSelect, m_rcZoomSrc)) {
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight; return;
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
if (m_bAdaptiveSize) {
m_rcZoomSrc.left = (int)(rcSelect.left * m_wZoom);
m_rcZoomSrc.top = (int)(rcSelect.top * m_hZoom);
m_rcZoomSrc.right = (int)(rcSelect.right * m_wZoom);
m_rcZoomSrc.bottom = (int)(rcSelect.bottom * m_hZoom);
} else {
m_rcZoomSrc.left = rcSelect.left + m_ulHScrollPos;
m_rcZoomSrc.top = rcSelect.top + m_ulVScrollPos;
m_rcZoomSrc.right = rcSelect.right + m_ulHScrollPos;
m_rcZoomSrc.bottom = rcSelect.bottom + m_ulVScrollPos;
} }
// 限制在原图范围内
m_rcZoomSrc.left = max(0L, min(m_rcZoomSrc.left, (LONG)srcW));
m_rcZoomSrc.top = max(0L, min(m_rcZoomSrc.top, (LONG)srcH));
m_rcZoomSrc.right = max(0L, min(m_rcZoomSrc.right, (LONG)srcW));
m_rcZoomSrc.bottom = max(0L, min(m_rcZoomSrc.bottom, (LONG)srcH));
// 进入放大状态 // 进入放大状态
m_bZoomedIn = true; m_bZoomedIn = true;
Invalidate(); Invalidate();
@@ -2897,6 +2897,145 @@ void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
} }
void CScreenSpyDlg::OnRButtonDown(UINT nFlags, CPoint point)
{
// 非控制模式下:右键框选 → 截图保存。控制模式下右键由 PreTranslateMessage 转发给客户端。
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full) {
// 与左键互斥:左键正在框选/拖拽时不接管右键,避免冲突
if (m_bSelectingZoom || m_bZoomDragging) {
return;
}
m_bSelectingShot = true;
m_ptShotStart = point;
m_ptShotCurrent = point;
SetCapture();
return;
}
__super::OnRButtonDown(nFlags, point);
}
void CScreenSpyDlg::OnRButtonUp(UINT nFlags, CPoint point)
{
if (!m_bIsCtrl && !m_bIsFirst && m_BitmapInfor_Full && m_bSelectingShot) {
ReleaseCapture();
m_bSelectingShot = false;
CRect rcSelect;
rcSelect.left = min(m_ptShotStart.x, point.x);
rcSelect.top = min(m_ptShotStart.y, point.y);
rcSelect.right = max(m_ptShotStart.x, point.x);
rcSelect.bottom = max(m_ptShotStart.y, point.y);
// 太小视为误触(与左键放大同阈值)
if (rcSelect.Width() < 20 || rcSelect.Height() < 20) {
Invalidate(FALSE);
return;
}
CRect rcImage;
if (ScreenRectToImageRect(rcSelect, rcImage) &&
rcImage.Width() > 0 && rcImage.Height() > 0)
{
SaveRegionScreenshot(rcImage);
}
Invalidate(FALSE); // 清掉绿色选框
return;
}
__super::OnRButtonUp(nFlags, point);
}
// 屏幕(窗口)选框 → 原图坐标,考虑放大状态、自适应、滚动
bool CScreenSpyDlg::ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage)
{
if (!m_BitmapInfor_Full) return false;
int srcW = m_BitmapInfor_Full->bmiHeader.biWidth;
int srcH = m_BitmapInfor_Full->bmiHeader.biHeight;
if (srcW <= 0 || srcH <= 0) return false;
if (m_bZoomedIn && !m_rcZoomSrc.IsRectEmpty()) {
// 放大状态:屏幕坐标 → 当前可视的子区域内的原图坐标
int dstW = m_CRect.Width();
int dstH = m_CRect.Height();
if (dstW <= 0 || dstH <= 0) return false;
double scaleX = (double)m_rcZoomSrc.Width() / dstW;
double scaleY = (double)m_rcZoomSrc.Height() / dstH;
rcImage.left = (int)(m_rcZoomSrc.left + rcScreen.left * scaleX);
rcImage.top = (int)(m_rcZoomSrc.top + rcScreen.top * scaleY);
rcImage.right = (int)(m_rcZoomSrc.left + rcScreen.right * scaleX);
rcImage.bottom = (int)(m_rcZoomSrc.top + rcScreen.bottom * scaleY);
} else if (m_bAdaptiveSize) {
rcImage.left = (int)(rcScreen.left * m_wZoom);
rcImage.top = (int)(rcScreen.top * m_hZoom);
rcImage.right = (int)(rcScreen.right * m_wZoom);
rcImage.bottom = (int)(rcScreen.bottom * m_hZoom);
} else {
rcImage.left = rcScreen.left + m_ulHScrollPos;
rcImage.top = rcScreen.top + m_ulVScrollPos;
rcImage.right = rcScreen.right + m_ulHScrollPos;
rcImage.bottom = rcScreen.bottom + m_ulVScrollPos;
}
// 限制在原图范围内
rcImage.left = max(0L, min(rcImage.left, (LONG)srcW));
rcImage.top = max(0L, min(rcImage.top, (LONG)srcH));
rcImage.right = max(0L, min(rcImage.right, (LONG)srcW));
rcImage.bottom = max(0L, min(rcImage.bottom, (LONG)srcH));
return true;
}
// 把原图中 [rcImage] 区域裁出来,写成独立 BMP24bpp 或 32bpp 由源图决定)
void CScreenSpyDlg::SaveRegionScreenshot(const CRect& rcImage)
{
if (!m_BitmapInfor_Full || !m_BitmapData_Full) return;
if (rcImage.Width() <= 0 || rcImage.Height() <= 0) return;
auto path = GetScreenShotPath(this, m_IPAddress, _TR("位图文件(*.bmp)|*.bmp|"), "bmp");
if (path.empty()) return;
// 源 DIB 是 BGR 24bpp 或 BGRA 32bppbottom-upbiHeight > 0
const BITMAPINFOHEADER& srcHdr = m_BitmapInfor_Full->bmiHeader;
int bpp = srcHdr.biBitCount;
if (bpp != 24 && bpp != 32) return; // 仅支持当前实际使用的两种位深
int srcW = srcHdr.biWidth;
int srcH = srcHdr.biHeight;
int srcStride = ((srcW * bpp + 31) / 32) * 4;
int dstW = rcImage.Width();
int dstH = rcImage.Height();
int dstStride = ((dstW * bpp + 31) / 32) * 4;
int dstSize = dstStride * dstH;
std::vector<BYTE> dstPixels(dstSize, 0);
const BYTE* srcBase = (const BYTE*)m_BitmapData_Full;
// bottom-up原图第 y 行(从顶起算)位于 srcBase + (srcH - 1 - y) * srcStride
int byteX = rcImage.left * (bpp / 8);
int copyBytes = dstW * (bpp / 8);
for (int y = 0; y < dstH; ++y) {
int srcRowFromTop = rcImage.top + y;
int srcRowOffset = (srcH - 1 - srcRowFromTop) * srcStride + byteX;
int dstRowOffset = (dstH - 1 - y) * dstStride;
memcpy(&dstPixels[dstRowOffset], &srcBase[srcRowOffset], copyBytes);
}
// 拼装 BITMAPINFO裁剪后只需要 BITMAPINFOHEADER24/32bpp 不需要调色板)
BITMAPINFO dstBmi = {};
dstBmi.bmiHeader = srcHdr;
dstBmi.bmiHeader.biWidth = dstW;
dstBmi.bmiHeader.biHeight = dstH;
dstBmi.bmiHeader.biSizeImage = dstSize;
dstBmi.bmiHeader.biCompression = BI_RGB;
if (WriteBitmap(&dstBmi, dstPixels.data(), path)) {
m_strSaveNotice = path;
m_nSaveNoticeTime = GetTickCount64();
}
}
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{ {
// Convert screen coordinates to client coordinates // Convert screen coordinates to client coordinates
@@ -2926,6 +3065,11 @@ void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
Invalidate(FALSE); // FALSE表示不擦除背景减少闪烁 Invalidate(FALSE); // FALSE表示不擦除背景减少闪烁
return; return;
} }
if (m_bSelectingShot) {
m_ptShotCurrent = point;
Invalidate(FALSE);
return;
}
if (m_bZoomDragging) { if (m_bZoomDragging) {
// 拖拽平移:计算偏移量并移动放大区域 // 拖拽平移:计算偏移量并移动放大区域
@@ -3060,9 +3204,14 @@ void CScreenSpyDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl) void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
{ {
m_bIsCtrl = ctrl; m_bIsCtrl = ctrl;
// 进入控制模式时重置放大状态 // 进入控制模式时重置放大状态 + 中止任何正在进行的右键截图框选
if (m_bIsCtrl && m_bZoomedIn) { if (m_bIsCtrl) {
ResetZoom(); if (m_bZoomedIn) ResetZoom();
if (m_bSelectingShot) {
m_bSelectingShot = false;
if (GetCapture() == this) ReleaseCapture();
Invalidate(FALSE);
}
} }
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO)); SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME查看模式启用本地 IME // 控制模式:禁用本地 IME查看模式启用本地 IME
@@ -3072,9 +3221,10 @@ void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
void CScreenSpyDlg::OnCaptureChanged(CWnd* pWnd) void CScreenSpyDlg::OnCaptureChanged(CWnd* pWnd)
{ {
// 捕获丢失时重置框选/拖拽状态 // 捕获丢失时重置框选/拖拽状态
if (m_bSelectingZoom || m_bZoomDragging) { if (m_bSelectingZoom || m_bZoomDragging || m_bSelectingShot) {
m_bSelectingZoom = false; m_bSelectingZoom = false;
m_bZoomDragging = false; m_bZoomDragging = false;
m_bSelectingShot = false;
Invalidate(); Invalidate();
} }
__super::OnCaptureChanged(pWnd); __super::OnCaptureChanged(pWnd);

View File

@@ -233,9 +233,16 @@ public:
CPoint m_ptZoomDragStart; // 拖拽起点(用于点击检测) CPoint m_ptZoomDragStart; // 拖拽起点(用于点击检测)
CPoint m_ptZoomDragLast; // 拖拽上一点(用于增量计算) CPoint m_ptZoomDragLast; // 拖拽上一点(用于增量计算)
// ========== 区域截图(右键框选) ==========
bool m_bSelectingShot = false; // 是否正在右键框选截图
CPoint m_ptShotStart; // 右键框选起点(屏幕坐标)
CPoint m_ptShotCurrent; // 右键框选当前点(屏幕坐标)
void ResetZoom(); // 重置放大状态 void ResetZoom(); // 重置放大状态
CPoint ScreenToImage(CPoint pt); // 屏幕坐标转原图坐标 CPoint ScreenToImage(CPoint pt); // 屏幕坐标转原图坐标
CPoint ImageToScreen(CPoint pt); // 原图坐标转屏幕坐标 CPoint ImageToScreen(CPoint pt); // 原图坐标转屏幕坐标
bool ScreenRectToImageRect(const CRect& rcScreen, CRect& rcImage); // 选框坐标→原图坐标
void SaveRegionScreenshot(const CRect& rcImage); // 保存裁剪区域为 BMP
CString m_aviFile; CString m_aviFile;
CBmpToAvi m_aviStream; CBmpToAvi m_aviStream;
@@ -312,6 +319,8 @@ public:
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
afx_msg void OnRButtonUp(UINT nFlags, CPoint point);
afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt);
afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnMouseLeave(); afx_msg void OnMouseLeave();
@@ -347,6 +356,7 @@ public:
virtual BOOL OnInitDialog(); virtual BOOL OnInitDialog();
afx_msg void OnClose(); afx_msg void OnClose();
afx_msg void OnPaint(); afx_msg void OnPaint();
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message); BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
afx_msg void OnSysCommand(UINT nID, LPARAM lParam); afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
virtual BOOL PreTranslateMessage(MSG* pMsg); virtual BOOL PreTranslateMessage(MSG* pMsg);

View File

@@ -1171,7 +1171,7 @@ WIN32
请选择目录=Language location 请选择目录=Language location
国际化(&N)=Internationalization 国际化(&N)=Internationalization
语言包目录(&D)=Language Pack Directory 语言包目录(&D)=Language Pack Directory
请通过“扩展”菜单指定语言包目录以支持多语言=Please specify the language pack directory via the "Extensions" menu to enable multi-language support. 请通过\"扩展\"菜单指定语言包目录以支持多语言=Please specify the language pack directory via the "Extensions" menu to enable multi-language support.
请选择[*.ico]图标文件或输入进程描述!=Please select an [*.ico] icon file or enter a process description! 请选择[*.ico]图标文件或输入进程描述!=Please select an [*.ico] icon file or enter a process description!
PE 编辑=PE Edit PE 编辑=PE Edit
PE 编辑(&R)=PE Edit(&R) PE 编辑(&R)=PE Edit(&R)

View File

@@ -1169,7 +1169,7 @@ WIN32
请选择目录=請選擇目錄 请选择目录=請選擇目錄
国际化(&N)=國際化 国际化(&N)=國際化
语言包目录(&D)=語言包目錄 语言包目录(&D)=語言包目錄
请通过“扩展”菜单指定语言包目录以支持多语言=請透過「擴充」選單指定語言包目錄,以支援多國語言。 请通过\"扩展\"菜单指定语言包目录以支持多语言=請透過「擴充」選單指定語言包目錄,以支援多國語言。
请选择[*.ico]图标文件或输入进程描述!=請選擇[*.ico]圖示檔案或輸入處理程序描述! 请选择[*.ico]图标文件或输入进程描述!=請選擇[*.ico]圖示檔案或輸入處理程序描述!
PE 编辑=PE 編輯 PE 编辑=PE 編輯
PE 编辑(&R)=PE 編輯(&R) PE 编辑(&R)=PE 編輯(&R)