diff --git a/client/KernelManager.cpp b/client/KernelManager.cpp index a6848cd..98eee32 100644 --- a/client/KernelManager.cpp +++ b/client/KernelManager.cpp @@ -272,6 +272,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param) FrpcParam* f = (FrpcParam*)user; Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed"); int r = 0; + uint64_t start = time(0); if (proc) { r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort, &CKernelManager::g_IsAppExit); @@ -279,7 +280,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param) else { This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), ""); } - if (r) { + if (r || (time(0)-start < 15)) { char buf[100]; sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r); Mprintf("%s\n", buf); @@ -295,6 +296,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param) FrpcParam* f = (FrpcParam*)user; Mprintf("MemoryGetProcAddress '%s' %s\n", info.Name, proc ? "success" : "failed"); int r = 0; + uint64_t start = time(0); if (proc) { r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort, &CKernelManager::g_IsAppExit); @@ -302,7 +304,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param) else { This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), ""); } - if (r) { + if (r || (time(0)-start < 15)) { char buf[100]; sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r); Mprintf("%s\n", buf); diff --git a/common/commands.h b/common/commands.h index 7d4991b..f3af1fe 100644 --- a/common/commands.h +++ b/common/commands.h @@ -330,6 +330,7 @@ enum { CMD_SET_GROUP = 242, // 修改分组 CMD_EXECUTE_DLL_NEW = 243, // 执行代码 CMD_PEER_TO_PEER = 244, // P2P通信 + TOKEN_CLIENTID = 245, }; enum MachineCommand { diff --git a/macos/install.sh b/macos/install.sh index ba49f20..cc87136 100644 --- a/macos/install.sh +++ b/macos/install.sh @@ -4,100 +4,101 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" GHOST_SRC="${1:-$SCRIPT_DIR/build/bin/ghost}" -GHOST_DST="/usr/local/bin/ghost" -PLIST_DST="/Library/LaunchDaemons/com.ghost.client.plist" +APP_DIR="/Applications/GhostClient.app" +APP_BIN="$APP_DIR/Contents/MacOS/ghost" -echo "=== Ghost Client 安装程序 ===" -echo "源文件: $GHOST_SRC" +echo "=== GhostClient 安装程序 ===" +echo "" # 检查源文件 if [ ! -f "$GHOST_SRC" ]; then - echo "" echo "错误: 找不到 $GHOST_SRC" echo "" echo "请先编译: ./build.sh" - echo "" echo "或指定路径: $0 " exit 1 fi +echo "源文件: $GHOST_SRC" +echo "" + set -e -# 1. 停止旧服务(只停止安装目录的,不影响调试目录) -echo "[1/6] 停止旧服务..." -sudo launchctl unload "$PLIST_DST" 2>/dev/null || true -sudo pkill -9 -f "$GHOST_DST" 2>/dev/null || true +# 1. 停止旧进程 +echo "[1/6] 停止旧进程..." +pkill -9 -f "$APP_BIN" 2>/dev/null || true -# 2. 复制程序 -echo "[2/6] 安装程序到 $GHOST_DST..." -sudo cp "$GHOST_SRC" "$GHOST_DST" -sudo chmod +x "$GHOST_DST" +# 2. 重置系统权限(关键步骤!避免权限缓存导致空白桌面) +echo "[2/6] 重置系统权限..." +echo " (这会清除屏幕录制和辅助功能的旧授权,需要重新授权)" +tccutil reset ScreenCapture 2>/dev/null || true +tccutil reset Accessibility 2>/dev/null || true -# 3. 清除隔离属性 -echo "[3/6] 清除隔离属性..." -sudo xattr -cr "$GHOST_DST" +# 3. 创建应用程序包 +echo "[3/6] 创建应用程序..." +sudo rm -rf "$APP_DIR" +sudo mkdir -p "$APP_DIR/Contents/MacOS" +sudo mkdir -p "$APP_DIR/Contents/Resources" -# 4. 签名 -echo "[4/6] 签名程序..." -sudo codesign --force --deep --sign - "$GHOST_DST" +# 复制 ghost 到 app bundle 内部 +sudo cp "$GHOST_SRC" "$APP_BIN" +sudo chmod +x "$APP_BIN" -# 5. 创建 launchd plist -echo "[5/6] 创建 launchd 服务..." -sudo tee "$PLIST_DST" > /dev/null << 'EOF' +# 创建 Info.plist +sudo tee "$APP_DIR/Contents/Info.plist" > /dev/null << 'EOF' - Label + CFBundleExecutable + ghost + CFBundleIdentifier com.ghost.client - - ProgramArguments - - /usr/local/bin/ghost - - - RunAtLoad + CFBundleName + GhostClient + CFBundleVersion + 1.0 + LSUIElement - - KeepAlive - - - StandardOutPath - /var/log/ghost.log - - StandardErrorPath - /var/log/ghost.log EOF -sudo chown root:wheel "$PLIST_DST" -sudo chmod 644 "$PLIST_DST" +# 4. 清除隔离属性 +echo "[4/6] 清除隔离属性..." +sudo xattr -cr "$APP_DIR" -# 6. 完成 -echo "[6/6] 安装完成!" +# 5. 签名应用 +echo "[5/6] 签名应用..." +sudo codesign --force --deep --sign - "$APP_DIR" + +# 6. 添加到登录项(开机自启) +echo "[6/7] 添加到登录项..." +osascript -e 'tell application "System Events" to delete login item "GhostClient"' 2>/dev/null || true +osascript -e 'tell application "System Events" to make login item at end with properties {path:"/Applications/GhostClient.app", hidden:true}' 2>/dev/null && echo " 已添加开机自启" || echo " 添加失败,请手动添加" + +# 7. 完成 +echo "[7/7] 安装完成!" echo "" echo "========================================" -echo "重要: 首次运行需要授权系统权限" +echo " 下一步: 授权系统权限" echo "========================================" echo "" -echo "请执行以下步骤:" +echo "1. 启动应用 (会自动弹出权限请求):" echo "" -echo "1. 手动运行以触发权限请求:" -echo " $GHOST_DST" +echo " open /Applications/GhostClient.app" echo "" -echo "2. 授权后按 Ctrl+C 停止程序(权限需重启生效)" +echo "2. 授权以下权限 (在系统设置中勾选 GhostClient):" +echo " - 屏幕录制: 允许捕获屏幕画面" +echo " - 辅助功能: 允许控制鼠标键盘" echo "" -echo "3. 启动服务:" -echo " sudo launchctl load $PLIST_DST" +echo "3. 授权后重启应用:" echo "" -echo "如未弹出授权对话框,手动添加:" -echo " 系统设置 > 隐私与安全性 > 屏幕录制 > 添加 ghost" -echo " 系统设置 > 隐私与安全性 > 辅助功能 > 添加 ghost" +echo " pkill -f GhostClient && open /Applications/GhostClient.app" echo "" -echo "常用命令:" -echo " 启动: sudo launchctl start com.ghost.client" -echo " 停止: sudo launchctl stop com.ghost.client" -echo " 卸载: sudo launchctl unload $PLIST_DST" -echo " 日志: tail -f /var/log/ghost.log" +echo "4. 查看日志确认运行状态:" +echo "" +echo " tail -f /tmp/ghost.log" +echo "" +echo "========================================" echo "" diff --git a/macos/uninstall.sh b/macos/uninstall.sh index b3b8788..8047bff 100644 --- a/macos/uninstall.sh +++ b/macos/uninstall.sh @@ -1,31 +1,32 @@ #!/bin/bash # macOS Ghost Client 卸载脚本 -echo "=== Ghost Client 卸载程序 ===" +APP_DIR="/Applications/GhostClient.app" -# 1. 停止并卸载 launchd 服务 -echo "[1/4] 停止服务..." -sudo launchctl unload /Library/LaunchDaemons/com.ghost.client.plist 2>/dev/null -launchctl unload ~/Library/LaunchAgents/com.ghost.client.plist 2>/dev/null - -# 2. 杀死残留进程 -echo "[2/4] 终止进程..." -sudo pkill -9 -f "/usr/local/bin/ghost" 2>/dev/null - -# 3. 删除文件 -echo "[3/4] 删除文件..." -sudo rm -f /Library/LaunchDaemons/com.ghost.client.plist -rm -f ~/Library/LaunchAgents/com.ghost.client.plist -sudo rm -f /usr/local/bin/ghost -rm -rf ~/.config/ghost -sudo rm -f /var/log/ghost.log - -# 4. 完成 -echo "[4/4] 卸载完成!" +echo "=== GhostClient 卸载程序 ===" echo "" -echo "注意: 系统权限(屏幕录制/辅助功能)未重置。" + +# 1. 停止进程 +echo "[1/3] 停止进程..." +pkill -9 -f "$APP_DIR" 2>/dev/null || true + +# 2. 删除文件 +echo "[2/3] 删除文件..." +sudo rm -rf "$APP_DIR" +rm -rf ~/.config/ghost 2>/dev/null || true +rm -f /tmp/ghost.log 2>/dev/null || true + +# 3. 移除登录项 +echo "[3/4] 移除登录项..." +osascript -e 'tell application "System Events" to delete login item "GhostClient"' 2>/dev/null || true + +# 4. 重置系统权限 +echo "[4/4] 重置系统权限..." +tccutil reset ScreenCapture 2>/dev/null || true +tccutil reset Accessibility 2>/dev/null || true + +echo "" +echo "========================================" +echo " 卸载完成" +echo "========================================" echo "" -echo "如需重置系统权限(会影响所有应用),请手动执行:" -echo " tccutil reset ScreenCapture" -echo " tccutil reset Accessibility" -echo " tccutil reset SystemPolicyAllFiles" diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index 0479ea0..4bed78b 100644 Binary files a/server/2015Remote/2015Remote.rc and b/server/2015Remote/2015Remote.rc differ diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index dad5ffe..f48064a 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -176,9 +176,9 @@ bool SupportsFileTransferV2(context* ctx) { } // 授权日志频率控制:首次必须记录,状态变化必须记录,相同状态每小时记录一次 -static bool ShouldLogAuth(const std::string& sn, bool success) { +static bool ShouldLogAuth(const std::string& sn, int success) { struct AuthLogState { - bool lastStatus; + int lastStatus; time_t lastLogTime; }; static std::map s_cache; @@ -4526,12 +4526,12 @@ bool IsDateInRange(const std::string& startDate, const std::string& endDate) return (today >= startDate && today <= endDate); } -BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired) +int CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired) { if (outExpired) *outExpired = false; if (sn.empty() || passcode.empty() || hmac == 0) { - return FALSE; + return 1; } auto v = splitString(passcode, '-'); if (v.size() == 6 || v.size() == 7) { @@ -4541,7 +4541,7 @@ BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, cons std::string hash256 = joinString(subvector, '-'); std::string fixedKey = getFixedLengthID(finalKey); if (hash256 != fixedKey) - return FALSE; + return 2; } static const char* superAdmin = getenv(BRAND_ENV_VAR); @@ -4550,14 +4550,13 @@ BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, cons Mprintf("请设置环境变量 " BRAND_ENV_VAR " 来给下级授权!\n"); } BOOL b = VerifyMessage(pwd, (BYTE*)passcode.c_str(), passcode.length(), hmac); - if (!b) return FALSE; + if (!b) return 3; auto list = StringToVector(passcode, '-', 2); BOOL valid = IsDateInRange(list[0], list[1]); std::string hmacStr = std::to_string(hmac); // 授权过期,更新或创建记录并标记为过期 if (!valid) { - Mprintf("授权已过期: %s\n", sn.c_str()); if (outExpired) *outExpired = true; // 签名有效但已过期 if (ctx != nullptr) { std::string ip = ctx->GetClientData(ONLINELIST_IP); @@ -4568,13 +4567,12 @@ BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, cons UpdateLicenseActivity(sn, passcode, hmacStr); } SetLicenseStatus(sn, LICENSE_STATUS_EXPIRED); - return FALSE; + return 4; } // 检查授权是否已被撤销 if (IsLicenseRevoked(sn)) { - Mprintf("授权已被撤销: %s\n", sn.c_str()); - return FALSE; + return 5; } // 授权成功时更新 license 活跃信息 @@ -4587,21 +4585,20 @@ BOOL CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, cons UpdateLicenseActivity(sn, passcode, hmacStr); } - return TRUE; + return 0; } -BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired) +int CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired) { if (outExpired) *outExpired = false; if (sn.empty() || passcode.empty() || hmacV2.empty()) { - return FALSE; + return 1; } // 检查 V2 前缀 if (hmacV2.substr(0, 3) != "v2:") { - Mprintf("V2 HMAC 格式错误: %s\n", hmacV2.c_str()); - return FALSE; + return 2; } // 检查公钥是否已配置(全零表示未配置) @@ -4613,15 +4610,13 @@ BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, co } } if (!keyConfigured) { - Mprintf("V2 公钥未配置,无法验证 V2 授权\n"); - return FALSE; + return 3; } // 使用 V2 验证 BOOL b = verifyPasswordV2(sn, passcode, hmacV2, g_LicensePublicKey); if (!b) { - Mprintf("V2 签名验证失败: %s\n", sn.c_str()); - return FALSE; + return 4; } auto list = StringToVector(passcode, '-', 2); @@ -4629,7 +4624,6 @@ BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, co // 授权过期 if (!valid) { - Mprintf("V2 授权已过期: %s\n", sn.c_str()); if (outExpired) *outExpired = true; // 签名有效但已过期 if (ctx != nullptr) { std::string ip = ctx->GetClientData(ONLINELIST_IP); @@ -4640,13 +4634,12 @@ BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, co UpdateLicenseActivity(sn, passcode, hmacV2); } SetLicenseStatus(sn, LICENSE_STATUS_EXPIRED); - return FALSE; + return 5; } // 检查授权是否已被撤销 if (IsLicenseRevoked(sn)) { - Mprintf("V2 授权已被撤销: %s\n", sn.c_str()); - return FALSE; + return 6; } // 授权成功时更新 license 活跃信息 @@ -4659,7 +4652,7 @@ BOOL CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, co UpdateLicenseActivity(sn, passcode, hmacV2); } - return TRUE; + return 0; } BOOL IsTrail(const std::string& passcode) @@ -5785,7 +5778,7 @@ std::tuple CMy2015RemoteDlg::VerifyClientAuth(context* h const std::string& sn, const std::string& passcode, uint64_t hmac, const std::string& hmacV2, const std::string& ip, const char* source) { - bool authorized = false; + BOOL authorized = -1; bool isV2 = false; bool isTrail = false; bool expired = false; @@ -5794,19 +5787,19 @@ std::tuple CMy2015RemoteDlg::VerifyClientAuth(context* h // V2 授权验证 isV2 = true; authorized = AuthorizeClientV2(host, sn, passcode, hmacV2, &expired); - if (authorized) { + if (authorized == 0) { if (host) { m_ClientMap->SetClientMapInteger(host->GetClientID(), MAP_AUTH, TRUE); } isTrail = IsTrail(passcode.c_str()); } if (ShouldLogAuth(sn, authorized)) { - if (authorized) { + if (authorized == 0) { Mprintf("[%s] %s V2 授权成功: %s [%s]\n", source, passcode.c_str(), sn.c_str(), ip.c_str()); std::string tip = passcode + std::string(_L(" V2 授权成功: ")) + sn + "[" + ip + "]"; PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL); } else { - Mprintf("[%s] %s V2 授权失败: %s [%s]\n", source, passcode.c_str(), sn.c_str(), ip.c_str()); + Mprintf("[%s] %s V2 授权失败 %d: %s [%s]\n", source, passcode.c_str(), authorized, sn.c_str(), ip.c_str()); std::string tip = passcode + std::string(_L(" V2 授权失败: ")) + sn + "[" + ip + "]"; PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL); } @@ -5815,26 +5808,26 @@ std::tuple CMy2015RemoteDlg::VerifyClientAuth(context* h // V1 授权验证 isV2 = false; authorized = AuthorizeClient(host, sn, passcode, hmac, &expired); - if (authorized) { + if (authorized == 0) { if (host) { m_ClientMap->SetClientMapInteger(host->GetClientID(), MAP_AUTH, TRUE); } isTrail = IsTrail(passcode.c_str()); } if (ShouldLogAuth(sn, authorized)) { - if (authorized) { + if (authorized == 0) { Mprintf("[%s] %s V1 授权成功: %s [%s]\n", source, passcode.c_str(), sn.c_str(), ip.c_str()); std::string tip = passcode + std::string(_L(" V1 授权成功: ")) + sn + "[" + ip + "]"; PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL); } else { - Mprintf("[%s] %s V1 授权失败: %s [%s]\n", source, passcode.c_str(), sn.c_str(), ip.c_str()); + Mprintf("[%s] %s V1 授权失败 %d: %s [%s]\n", source, passcode.c_str(), authorized, sn.c_str(), ip.c_str()); std::string tip = passcode + std::string(_L(" V1 授权失败: ")) + sn + "[" + ip + "]"; PostMessageA(WM_SHOWMESSAGE, (WPARAM)new CharMsg(tip.c_str()), NULL); } } } - return std::make_tuple(authorized, isV2, isTrail, expired); + return std::make_tuple(authorized==0, isV2, isTrail, expired); } // 检查并发送预设续期(多点验证) @@ -8609,6 +8602,22 @@ context* CMy2015RemoteDlg::FindHostByIP(const std::string& ip) return NULL; } +uint64_t CMy2015RemoteDlg::FindClientIDByIP(const std::string& ip) +{ + CString clientIP(ip.c_str()); + uint64_t clientID = 0; + EnterCriticalSection(&m_cs); + for (auto i = m_HostList.begin(); i != m_HostList.end(); ++i) { + context* ContextObject = *i; + if (ContextObject->GetClientData(ONLINELIST_IP) == clientIP || ContextObject->GetAdditionalData(RES_CLIENT_PUBIP) == clientIP) { + clientID = ContextObject->GetClientID(); + break; + } + } + LeaveCriticalSection(&m_cs); + return clientID; +} + LRESULT CMy2015RemoteDlg::InjectShellcode(WPARAM wParam, LPARAM lParam) { std::string* ip = (std::string*)wParam; diff --git a/server/2015Remote/2015RemoteDlg.h b/server/2015Remote/2015RemoteDlg.h index 388cd12..79fd280 100644 --- a/server/2015Remote/2015RemoteDlg.h +++ b/server/2015Remote/2015RemoteDlg.h @@ -215,8 +215,8 @@ public: MasterSettings m_settings; static BOOL CALLBACK NotifyProc(CONTEXT_OBJECT* ContextObject); static BOOL CALLBACK OfflineProc(CONTEXT_OBJECT* ContextObject); - BOOL AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr); - BOOL AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr); + int AuthorizeClient(context* ctx, const std::string& sn, const std::string& passcode, uint64_t hmac, bool* outExpired = nullptr); + int AuthorizeClientV2(context* ctx, const std::string& sn, const std::string& passcode, const std::string& hmacV2, bool* outExpired = nullptr); VOID MessageHandle(CONTEXT_OBJECT* ContextObject); VOID SendSelectedCommand(PBYTE szBuffer, ULONG ulLength, contextModifier cb = NULL, void* user=NULL); VOID SendAllCommand(PBYTE szBuffer, ULONG ulLength); @@ -255,6 +255,7 @@ public: CGridDialog * m_gridDlg = NULL; std::vector m_DllList; context* FindHostByIP(const std::string& ip); + uint64_t FindClientIDByIP(const std::string& ip); // 线程安全:在锁内获取ID void InjectTinyRunDll(const std::string& ip, int pid); NOTIFYICONDATA m_Nid; HANDLE m_hExit; diff --git a/server/2015Remote/FileManagerDlg.cpp b/server/2015Remote/FileManagerDlg.cpp index 72ff77f..5a60fb3 100644 --- a/server/2015Remote/FileManagerDlg.cpp +++ b/server/2015Remote/FileManagerDlg.cpp @@ -25,6 +25,26 @@ static UINT indicators[] = { #define MAX_SEND_BUFFER 65535 #define MAX_RECV_BUFFER 65535 +// 静态成员变量定义 - 历史路径记录 +CString CFileManagerDlg::s_strLocalHistoryPath; +std::map CFileManagerDlg::s_mapRemoteHistoryPath; +CLock CFileManagerDlg::s_lockHistory; + +// 获取有效的客户端ID:优先用 m_ClientID,否则通过 IP 找主连接 +uint64_t CFileManagerDlg::GetClientID() const +{ + // 优先使用已设置的 m_ClientID(未来 TOKEN_CLIENTID 会设置这个) + if (m_ClientID != 0) { + return m_ClientID; + } + // 回退:通过 IP 找主连接获取 ClientID(线程安全) + if (g_2015RemoteDlg && m_ContextObject) { + std::string peerIP = m_ContextObject->GetPeerName(); + return g_2015RemoteDlg->FindClientIDByIP(peerIP); + } + return 0; +} + typedef struct { LVITEM* plvi; CString sCol2; @@ -137,10 +157,12 @@ BEGIN_MESSAGE_MAP(CFileManagerDlg, CDialog) ON_COMMAND(IDT_LOCAL_DOWNLOADS, OnLocalDownloads) ON_COMMAND(IDT_LOCAL_HOME, OnLocalHome) ON_COMMAND(IDT_LOCAL_SEARCH, OnLocalSearch) + ON_COMMAND(IDT_LOCAL_HISTORY, OnLocalHistory) ON_COMMAND(IDT_REMOTE_DESKTOP, OnRemoteDesktop) ON_COMMAND(IDT_REMOTE_DOWNLOADS, OnRemoteDownloads) ON_COMMAND(IDT_REMOTE_HOME, OnRemoteHome) ON_COMMAND(IDT_REMOTE_SEARCH, OnRemoteSearch) + ON_COMMAND(IDT_REMOTE_HISTORY, OnRemoteHistory) ON_COMMAND(IDM_TRANSFER, OnTransfer) ON_COMMAND(IDM_RENAME, OnRename) ON_NOTIFY(LVN_ENDLABELEDIT, IDC_LIST_LOCAL, OnEndlabeleditListLocal) @@ -494,6 +516,12 @@ void CFileManagerDlg::FixedLocalFileList(CString directory) } ShowMessage(_TRF("本地:装载目录 %s 完成"), m_Local_Path); + + // 记录本地历史路径 + if (m_Local_Path.GetLength() > 0) { + CAutoCLock lock(s_lockHistory); + s_strLocalHistoryPath = m_Local_Path; + } } void CFileManagerDlg::DropItemOnList(CListCtrl* pDragList, CListCtrl* pDropList) @@ -966,6 +994,8 @@ void CFileManagerDlg::OnReceiveComplete() ShowMessage(_TRF("搜索 \"%s\" 在 %s 完成,共 %d 个结果 (耗时 %d秒)"), m_strSearchName, m_strSearchPath, m_nSearchResultCount, dwElapsed); } break; + case TOKEN_CLIENTID: + break; default: SendException(); break; @@ -1024,6 +1054,13 @@ void CFileManagerDlg::GetRemoteFileList(CString directory) m_Remote_Directory_ComboBox.InsertStringL(0, m_Remote_Path); m_Remote_Directory_ComboBox.SetCurSel(0); + // 记录远程历史路径(按客户端ID区分) + uint64_t clientID = GetClientID(); + if (m_Remote_Path.GetLength() > 0 && clientID != 0) { + CAutoCLock lock(s_lockHistory); + s_mapRemoteHistoryPath[clientID] = m_Remote_Path; + } + // 得到返回数据前禁窗口 m_list_remote.EnableWindow(FALSE); m_ProgressCtrl->SetPos(0); @@ -1594,6 +1631,57 @@ void CFileManagerDlg::OnRemoteSearch() } } +void CFileManagerDlg::OnLocalHistory() +{ + // 跳转到上次打开的本地文件夹 + CString historyPath; + { + CAutoCLock lock(s_lockHistory); + historyPath = s_strLocalHistoryPath; + } + + if (historyPath.IsEmpty()) { + ShowMessage(_TR("没有本地历史记录")); + return; + } + + // 检查目录是否存在 + if (GetFileAttributesA(historyPath) == INVALID_FILE_ATTRIBUTES) { + ShowMessage(_TRF("历史目录不存在: %s"), historyPath); + return; + } + + m_Local_Path = historyPath; + FixedLocalFileList("."); +} + +void CFileManagerDlg::OnRemoteHistory() +{ + // 跳转到上次打开的远程文件夹(按客户端ID区分) + uint64_t clientID = GetClientID(); + if (clientID == 0) { + ShowMessage(_TR("无法识别远程主机")); + return; + } + + CString historyPath; + { + CAutoCLock lock(s_lockHistory); + auto it = s_mapRemoteHistoryPath.find(clientID); + if (it != s_mapRemoteHistoryPath.end()) { + historyPath = it->second; + } + } + + if (historyPath.IsEmpty()) { + ShowMessage(_TR("没有远程历史记录")); + return; + } + + m_Remote_Path = historyPath; + GetRemoteFileList("."); +} + void CFileManagerDlg::OnLocalView() { // TODO: Add your command handler code here diff --git a/server/2015Remote/FileManagerDlg.h b/server/2015Remote/FileManagerDlg.h index bbf3de7..e5381c1 100644 --- a/server/2015Remote/FileManagerDlg.h +++ b/server/2015Remote/FileManagerDlg.h @@ -7,6 +7,7 @@ #include "IOCPServer.h" #include "SortListCtrl.h" +#include "../../common/locker.h" #include #include #include @@ -246,10 +247,12 @@ protected: afx_msg void OnLocalDownloads(); afx_msg void OnLocalHome(); afx_msg void OnLocalSearch(); + afx_msg void OnLocalHistory(); afx_msg void OnRemoteDesktop(); afx_msg void OnRemoteDownloads(); afx_msg void OnRemoteHome(); afx_msg void OnRemoteSearch(); + afx_msg void OnRemoteHistory(); afx_msg void OnTransferV2ToRemote(); // V2: 本地文件传输到远程 afx_msg void OnTransferV2ToLocal(); // V2: 远程文件传输到本地 //}}AFX_MSG @@ -274,6 +277,14 @@ private: void EnableControl(BOOL bEnable = TRUE); void CollectFilesRecursive(const std::string& dirPath, std::vector& files); float m_fScalingFactor; + + // 历史路径记录(静态,跨实例共享) + static CString s_strLocalHistoryPath; + static std::map s_mapRemoteHistoryPath; + static CLock s_lockHistory; // 保护历史路径的锁 + + // 获取有效的客户端ID(优先用 m_ClientID,否则通过 IP 找主连接) + uint64_t GetClientID() const; public: afx_msg void OnFilemangerCompress(); afx_msg void OnFilemangerUncompress(); diff --git a/server/2015Remote/IOCPServer.h b/server/2015Remote/IOCPServer.h index e4e3e51..c340a68 100644 --- a/server/2015Remote/IOCPServer.h +++ b/server/2015Remote/IOCPServer.h @@ -228,7 +228,7 @@ public: { return m_bIsClosed; } - uint64_t GetClientID() const { + virtual uint64_t GetClientID() const { return m_ClientID; } BOOL SayByeBye() diff --git a/server/2015Remote/WebPage.h b/server/2015Remote/WebPage.h index 47e3e9d..1e6bb2c 100644 --- a/server/2015Remote/WebPage.h +++ b/server/2015Remote/WebPage.h @@ -1846,13 +1846,15 @@ inline std::string GetWebPageHTML() { // Remote cursor mapping (Windows cursor index -> CSS cursor) // Index matches CursorInfo.h: IDC_APPSTARTING(0) to IDC_WAIT(15), 254=custom, 255=unsupported + // Custom I-beam cursor with white fill and black stroke for visibility on any background + const ibeamCursor = "url('data:image/svg+xml,') 8 12, text"; const cursorMap = [ 'progress', // 0: IDC_APPSTARTING 'default', // 1: IDC_ARROW 'crosshair', // 2: IDC_CROSS 'pointer', // 3: IDC_HAND 'help', // 4: IDC_HELP - 'text', // 5: IDC_IBEAM + ibeamCursor, // 5: IDC_IBEAM - custom cursor with outline 'default', // 6: IDC_ICON (no direct CSS equivalent) 'not-allowed', // 7: IDC_NO 'default', // 8: IDC_SIZE (deprecated, use default) diff --git a/server/2015Remote/lang/en_US.ini b/server/2015Remote/lang/en_US.ini index 6ccea6e..b909595 100644 --- a/server/2015Remote/lang/en_US.ini +++ b/server/2015Remote/lang/en_US.ini @@ -1819,3 +1819,7 @@ IOCP бΪգ޷=Plugin list is empty, cannot create trigger ѡһ=Please select at least one plugin +ûбʷ¼=No local history +ʷĿ¼: %s=History folder not exist: %s +޷ʶԶ=Unknown remote machine +ûԶʷ¼=No remote history diff --git a/server/2015Remote/lang/zh_TW.ini b/server/2015Remote/lang/zh_TW.ini index 90c5001..ec89467 100644 --- a/server/2015Remote/lang/zh_TW.ini +++ b/server/2015Remote/lang/zh_TW.ini @@ -1810,3 +1810,7 @@ IOCP << Ƴ=<< Ƴ бΪգ޷=бգo|l ѡһ=Ոxһ +ûбʷ¼=ûбʷ¼ +ʷĿ¼: %s=ʷĿ¼: %s +޷ʶԶ=޷ʶԶ +ûԶʷ¼=ûԶʷ¼ diff --git a/server/2015Remote/res/ToolBar_Disable.bmp b/server/2015Remote/res/ToolBar_Disable.bmp index 65482d6..5636b6e 100644 Binary files a/server/2015Remote/res/ToolBar_Disable.bmp and b/server/2015Remote/res/ToolBar_Disable.bmp differ diff --git a/server/2015Remote/res/ToolBar_Enable.bmp b/server/2015Remote/res/ToolBar_Enable.bmp index 50f9cb4..c630499 100644 Binary files a/server/2015Remote/res/ToolBar_Enable.bmp and b/server/2015Remote/res/ToolBar_Enable.bmp differ diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index 29e61ee..ba8079f 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -506,6 +506,8 @@ #define IDT_REMOTE_DOWNLOADS 2235 #define IDT_REMOTE_HOME 2236 #define IDT_REMOTE_SEARCH 2237 +#define IDT_LOCAL_HISTORY 2238 +#define IDT_REMOTE_HISTORY 2239 #define IDC_BUTTON_SAVE_LICENSE 2240 #define IDC_LICENSE_LIST 2241 #define IDC_COMBO_VERSION 2245